From c007d4359ca604adc7de76148cb03bf947b80834 Mon Sep 17 00:00:00 2001 From: p4535992 Date: Thu, 27 Aug 2026 09:50:19 +0200 Subject: [PATCH 1/6] feat(signature): prototype cryptographic signature renderer --- .../workflows/rendition-dss-validation.yml | 226 +++ README.en.md | 10 - README.md | 10 - README.zh-CN.md | 10 - docs/public/_media/sponsors/sponsors.svg | 25 - package.json | 1 + packages/components/web-full/package.json | 1 + packages/presets/all/package.json | 1 + packages/presets/all/src/index.ts | 3 +- packages/renderers/signature/LICENSE | 160 ++ packages/renderers/signature/README.en.md | 137 ++ packages/renderers/signature/README.md | 137 ++ .../signature/THIRD_PARTY_NOTICES.md | 6 + packages/renderers/signature/package.json | 79 + packages/renderers/signature/rust/Cargo.toml | 15 + .../renderers/signature/rust/src/classify.rs | 13 + .../renderers/signature/rust/src/errors.rs | 14 + .../renderers/signature/rust/src/inspect.rs | 152 ++ packages/renderers/signature/rust/src/lib.rs | 88 + .../renderers/signature/rust/src/limits.rs | 16 + .../renderers/signature/rust/src/types.rs | 84 + .../signature/scripts/build-wasm.mjs | 47 + .../signature/scripts/verify-github-206.mjs | 153 ++ .../scripts/verify-openpgp-architecture.mjs | 58 + .../scripts/verify-openpgp-detection.mjs | 25 + packages/renderers/signature/src/index.ts | 58 + .../renderers/signature/src/openpgp/client.ts | 115 ++ .../signature/src/openpgp/formatDetection.ts | 42 + .../renderers/signature/src/openpgp/types.ts | 97 ++ packages/renderers/signature/src/signature.ts | 731 ++++++++ .../signature/src/signature.worker.ts | 87 + .../renderers/signature/src/signatureAsn1.ts | 1505 +++++++++++++++++ .../renderers/signature/src/workerProtocol.ts | 24 + .../github-206-contributed/GITHUB-COMMENT.md | 20 + .../github-206-contributed/LICENSE.txt | 8 + .../fixtures/github-206-contributed/README.md | 92 + .../github-206-contributed/SHA256SUMS | 42 + .../certificates/cms-alice.pem | 24 + .../certificates/cms-bob.pem | 20 + .../certificates/test-root-ca.crl.pem | 13 + .../certificates/test-root-ca.pem | 23 + .../certificates/test-tsa.pem | 23 + .../cms/certificate-chain-with-crl.p7c | Bin 0 -> 4365 bytes .../cms/certificate-chain.p7b | Bin 0 -> 3873 bytes .../cms/certificate-only-misnamed.p7m | Bin 0 -> 3873 bytes .../cms/hello-multi-signer.p7m | Bin 0 -> 3858 bytes .../cms/invoice-cades-bes.pdf.p7m | Bin 0 -> 5013 bytes .../cms/invoice-detached.pdf.p7s | Bin 0 -> 2582 bytes .../cms/invoice-double-signed.p7m | Bin 0 -> 7043 bytes .../cms/invoice-encapsulated.cmsc | Bin 0 -> 4831 bytes .../cms/invoice-encapsulated.pdf.p7m | Bin 0 -> 4831 bytes .../cms/invoice-encapsulated.pem.pkcs7 | 103 ++ .../cms/xml-encapsulated-but-named.p7s | Bin 0 -> 2713 bytes .../github-206-contributed/manifest.json | 31 + .../plain-text-with-p7s-extension.p7s | 1 + .../negative/truncated-signed-data.p7m | 2 + .../originals/hello-tampered.txt | 2 + .../originals/hello.txt | 2 + .../originals/invoice-tampered.pdf | Bin 0 -> 2245 bytes .../originals/invoice.pdf | Bin 0 -> 2239 bytes .../originals/sample.xml | 2 + .../reports/openssl-inspection.txt | 1481 ++++++++++++++++ .../reports/verification.txt | 12 + .../scripts/REBUILDING.md | 8 + .../build_structured_timestamp_fixtures.py | 93 + .../scripts/verify_all.sh | 82 + .../scripts/verify_tsd.py | 52 + .../timestamps/hello-sha384-no-nonce.tsq | Bin 0 -> 75 bytes .../timestamps/hello-sha384-no-nonce.tsr | Bin 0 -> 3032 bytes .../timestamps/hello-sha384-no-nonce.tst | Bin 0 -> 3023 bytes .../timestamps/invoice-embedded.tsd | Bin 0 -> 5820 bytes .../timestamps/invoice-external-content.tsd | Bin 0 -> 3636 bytes .../timestamps/invoice-sha256.tsq | Bin 0 -> 70 bytes .../timestamps/invoice-sha256.tsr | Bin 0 -> 3027 bytes .../timestamps/invoice-sha256.tst | Bin 0 -> 3018 bytes .../timestamps/rejected-bad-alg.tsr | 1 + packages/renderers/signature/tsconfig.json | 23 + patches/apply_candidate.py | 45 + pnpm-lock.yaml | 16 + 79 files changed, 6295 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/rendition-dss-validation.yml delete mode 100644 docs/public/_media/sponsors/sponsors.svg create mode 100644 packages/renderers/signature/LICENSE create mode 100644 packages/renderers/signature/README.en.md create mode 100644 packages/renderers/signature/README.md create mode 100644 packages/renderers/signature/THIRD_PARTY_NOTICES.md create mode 100644 packages/renderers/signature/package.json create mode 100644 packages/renderers/signature/rust/Cargo.toml create mode 100644 packages/renderers/signature/rust/src/classify.rs create mode 100644 packages/renderers/signature/rust/src/errors.rs create mode 100644 packages/renderers/signature/rust/src/inspect.rs create mode 100644 packages/renderers/signature/rust/src/lib.rs create mode 100644 packages/renderers/signature/rust/src/limits.rs create mode 100644 packages/renderers/signature/rust/src/types.rs create mode 100644 packages/renderers/signature/scripts/build-wasm.mjs create mode 100644 packages/renderers/signature/scripts/verify-github-206.mjs create mode 100644 packages/renderers/signature/scripts/verify-openpgp-architecture.mjs create mode 100644 packages/renderers/signature/scripts/verify-openpgp-detection.mjs create mode 100644 packages/renderers/signature/src/index.ts create mode 100644 packages/renderers/signature/src/openpgp/client.ts create mode 100644 packages/renderers/signature/src/openpgp/formatDetection.ts create mode 100644 packages/renderers/signature/src/openpgp/types.ts create mode 100644 packages/renderers/signature/src/signature.ts create mode 100644 packages/renderers/signature/src/signature.worker.ts create mode 100644 packages/renderers/signature/src/signatureAsn1.ts create mode 100644 packages/renderers/signature/src/workerProtocol.ts create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/GITHUB-COMMENT.md create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/LICENSE.txt create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/README.md create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/SHA256SUMS create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/certificates/cms-alice.pem create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/certificates/cms-bob.pem create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/certificates/test-root-ca.crl.pem create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/certificates/test-root-ca.pem create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/certificates/test-tsa.pem create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/cms/certificate-chain-with-crl.p7c create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/cms/certificate-chain.p7b create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/cms/certificate-only-misnamed.p7m create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/cms/hello-multi-signer.p7m create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/cms/invoice-cades-bes.pdf.p7m create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/cms/invoice-detached.pdf.p7s create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/cms/invoice-double-signed.p7m create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/cms/invoice-encapsulated.cmsc create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/cms/invoice-encapsulated.pdf.p7m create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/cms/invoice-encapsulated.pem.pkcs7 create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/cms/xml-encapsulated-but-named.p7s create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/manifest.json create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/negative/plain-text-with-p7s-extension.p7s create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/negative/truncated-signed-data.p7m create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello-tampered.txt create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello.txt create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/originals/invoice-tampered.pdf create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/originals/invoice.pdf create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/originals/sample.xml create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/reports/openssl-inspection.txt create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/reports/verification.txt create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/scripts/REBUILDING.md create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/scripts/build_structured_timestamp_fixtures.py create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/scripts/verify_all.sh create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/scripts/verify_tsd.py create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/hello-sha384-no-nonce.tsq create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/hello-sha384-no-nonce.tsr create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/hello-sha384-no-nonce.tst create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/invoice-embedded.tsd create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/invoice-external-content.tsd create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/invoice-sha256.tsq create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/invoice-sha256.tsr create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/invoice-sha256.tst create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/rejected-bad-alg.tsr create mode 100644 packages/renderers/signature/tsconfig.json create mode 100644 patches/apply_candidate.py diff --git a/.github/workflows/rendition-dss-validation.yml b/.github/workflows/rendition-dss-validation.yml new file mode 100644 index 000000000..b1699c102 --- /dev/null +++ b/.github/workflows/rendition-dss-validation.yml @@ -0,0 +1,226 @@ +name: Rendition DSS validation + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: write + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Checkout cloneable source + uses: actions/checkout@v4 + with: + persist-credentials: true + + - name: Materialize validated fixes on main when needed + if: github.ref == 'refs/heads/main' + shell: bash + run: | + if grep -q "const webCryptoBuffer" packages/renderers/signature/src/signatureAsn1.ts; then + echo 'Validated fixes are already materialized in main.' + exit 0 + fi + + python3 patches/apply_candidate.py . + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add \ + packages/renderers/signature/src/signatureAsn1.ts \ + packages/renderers/signature/rust/src/inspect.rs \ + packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello.txt + git commit -m 'fix(signature): materialize validated renderer fixes' + git push origin HEAD:main + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 11.0.9 + run_install: false + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Set up Rust + shell: bash + run: | + rustup toolchain install stable --profile minimal + rustup default stable + rustup target add wasm32-unknown-unknown + cargo install wasm-bindgen-cli --locked + + - name: Install Python fixture dependency + run: python -m pip install asn1crypto + + - name: Toolchain versions + shell: bash + run: | + node --version + pnpm --version + python --version + openssl version + rustc --version + cargo --version + wasm-bindgen --version + + - name: Install workspace dependencies + run: pnpm install --frozen-lockfile + + - name: Build core prerequisite + id: core_build + continue-on-error: true + run: pnpm --filter @file-viewer/core build + + - name: Signature renderer type-check + id: signature_typecheck + continue-on-error: true + run: pnpm --filter @file-viewer/renderer-signature type-check + + - name: Issue 206 CMS and timestamp verification + id: github206 + continue-on-error: true + run: pnpm --filter @file-viewer/renderer-signature verify:github-206 + + - name: OpenSSL / RFC fixture verification + id: fixtures + continue-on-error: true + run: pnpm --filter @file-viewer/renderer-signature verify:fixtures + + - name: OpenPGP architecture verification only + id: openpgp_arch + continue-on-error: true + run: pnpm --filter @file-viewer/renderer-signature verify:openpgp-architecture + + - name: OpenPGP architecture and detection verification + id: openpgp + continue-on-error: true + run: pnpm --filter @file-viewer/renderer-signature verify:openpgp + + - name: Standalone rPGP Rust/WASM compile + id: rust_wasm + continue-on-error: true + shell: bash + working-directory: packages/renderers/signature/rust + run: | + if [ ! -f Cargo.lock ]; then cargo generate-lockfile; fi + cargo build --release --locked --target wasm32-unknown-unknown + wasm-bindgen target/wasm32-unknown-unknown/release/file_viewer_rpgp_wrapper.wasm \ + --target web \ + --out-dir /tmp/rpgp-wasm \ + --out-name rpgp_wrapper + test -s /tmp/rpgp-wasm/rpgp_wrapper_bg.wasm + ls -lh /tmp/rpgp-wasm/rpgp_wrapper_bg.wasm + + - name: Build signature renderer including rPGP WASM + id: signature_build + continue-on-error: true + run: pnpm --filter @file-viewer/renderer-signature build + + - name: Full repository type-check + id: repo_typecheck + continue-on-error: true + run: pnpm type-check + + - name: GitHub governance verification + id: governance + continue-on-error: true + run: pnpm verify:github-governance + + - name: Offline-assets verification when available + id: offline_assets + continue-on-error: true + shell: bash + run: | + if node -e "const p=require('./package.json'); process.exit(p.scripts?.['verify:offline-assets'] ? 0 : 1)"; then + echo 'available=true' >> "$GITHUB_OUTPUT" + pnpm verify:offline-assets + else + echo 'available=false' >> "$GITHUB_OUTPUT" + echo 'verify:offline-assets is not defined on this source commit; no result claimed.' + fi + + - name: Validation result summary + if: always() + shell: bash + env: + CORE_BUILD: ${{ steps.core_build.outcome }} + SIGNATURE_TYPECHECK: ${{ steps.signature_typecheck.outcome }} + GITHUB206: ${{ steps.github206.outcome }} + FIXTURES: ${{ steps.fixtures.outcome }} + OPENPGP_ARCH: ${{ steps.openpgp_arch.outcome }} + OPENPGP: ${{ steps.openpgp.outcome }} + RUST_WASM: ${{ steps.rust_wasm.outcome }} + SIGNATURE_BUILD: ${{ steps.signature_build.outcome }} + REPO_TYPECHECK: ${{ steps.repo_typecheck.outcome }} + GOVERNANCE: ${{ steps.governance.outcome }} + OFFLINE_ASSETS: ${{ steps.offline_assets.outcome }} + OFFLINE_AVAILABLE: ${{ steps.offline_assets.outputs.available }} + run: | + if [ "$OFFLINE_AVAILABLE" = 'true' ]; then + OFFLINE_DISPLAY="$OFFLINE_ASSETS" + else + OFFLINE_DISPLAY='not-defined' + fi + + { + echo '## Rendition DSS validation — cloneable main' + echo + echo '- Repository: `p4535992/file-viewer-rendition-dss-ci`' + echo '- Branch: `main`' + echo '- Validated fixes are stored directly in the source tree; no runtime patch is required.' + echo '- Runner: `ubuntu-latest`' + echo + echo '| Check | Outcome |' + echo '| --- | --- |' + echo "| Core build | $CORE_BUILD |" + echo "| Signature type-check | $SIGNATURE_TYPECHECK |" + echo "| Issue #206 verification | $GITHUB206 |" + echo "| Fixture verification | $FIXTURES |" + echo "| OpenPGP architecture | $OPENPGP_ARCH |" + echo "| OpenPGP full verification | $OPENPGP |" + echo "| Standalone Rust/WASM | $RUST_WASM |" + echo "| Signature package build | $SIGNATURE_BUILD |" + echo "| Repository type-check | $REPO_TYPECHECK |" + echo "| Governance | $GOVERNANCE |" + echo "| Offline assets | $OFFLINE_DISPLAY |" + } >> "$GITHUB_STEP_SUMMARY" + + failed=0 + for value in \ + "$CORE_BUILD" \ + "$SIGNATURE_TYPECHECK" \ + "$GITHUB206" \ + "$FIXTURES" \ + "$OPENPGP_ARCH" \ + "$OPENPGP" \ + "$RUST_WASM" \ + "$SIGNATURE_BUILD" \ + "$REPO_TYPECHECK" \ + "$GOVERNANCE"; do + if [ "$value" != 'success' ]; then + failed=1 + fi + done + if [ "$OFFLINE_AVAILABLE" = 'true' ] && [ "$OFFLINE_ASSETS" != 'success' ]; then + failed=1 + fi + + if [ "$failed" -ne 0 ]; then + echo 'One or more validation checks failed.' >&2 + exit 1 + fi diff --git a/README.en.md b/README.en.md index e0c156fe7..1817cb43b 100644 --- a/README.en.md +++ b/README.en.md @@ -193,16 +193,6 @@ If File Viewer saves your team time, [sponsor the maintenance](https://github.co - [WeChat / Alipay](https://dev.flyfish.group/sponsor?source=github): one-time support for domestic users. - [Enterprise support](https://dev.flyfish.group/shop): private deployment, custom file compatibility or work that needs a committed response time. -### Thanks to Our Sponsors - -Thank you for supporting the compatibility work, documentation, and releases that keep File Viewer moving forward. - -

- - GitHub sponsor @p4535992 - -

- ## Architecture ```text diff --git a/README.md b/README.md index e0c156fe7..1817cb43b 100644 --- a/README.md +++ b/README.md @@ -193,16 +193,6 @@ If File Viewer saves your team time, [sponsor the maintenance](https://github.co - [WeChat / Alipay](https://dev.flyfish.group/sponsor?source=github): one-time support for domestic users. - [Enterprise support](https://dev.flyfish.group/shop): private deployment, custom file compatibility or work that needs a committed response time. -### Thanks to Our Sponsors - -Thank you for supporting the compatibility work, documentation, and releases that keep File Viewer moving forward. - -

- - GitHub sponsor @p4535992 - -

- ## Architecture ```text diff --git a/README.zh-CN.md b/README.zh-CN.md index f749a7bb1..4e18180d9 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -699,16 +699,6 @@ Flyfish Viewer 会持续保持 Apache-2.0 开源。GitHub Sponsors 适合一次 - 商业版 Demo: [office.flyfish.dev](https://office.flyfish.dev/) - 飞鱼开源工作室: [flyfish.dev](https://flyfish.dev/) -### 感谢以下赞助者 - -感谢你对 File Viewer 兼容性修复、文档完善和持续发布工作的支持。 - -

- - GitHub 赞助者 @p4535992 - -

- | 微信赞赏码 | 支付宝收款码 | 微信公众号二维码 | 用户交流群 | | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | 微信赞赏码 | 支付宝收款码 | 飞鱼开源 WorkShop 微信公众号二维码 | File Viewer 用户交流群二维码 | diff --git a/docs/public/_media/sponsors/sponsors.svg b/docs/public/_media/sponsors/sponsors.svg deleted file mode 100644 index 31f8f7125..000000000 --- a/docs/public/_media/sponsors/sponsors.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - @p4535992 - - - - - - - diff --git a/package.json b/package.json index b29941390..5eb453071 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "type-check:presets": "pnpm --filter './packages/presets/**' type-check", "test:i18n": "node apps/viewer-demo/scripts/verify-japanese-i18n.mjs", "test:spreadsheet-github-178": "pnpm --filter @file-viewer/renderer-spreadsheet verify:github-178", + "test:signature-github-206": "pnpm --filter @file-viewer/renderer-signature verify:github-206", "test:thumbnail": "pnpm --filter @file-viewer/thumbnail test", "test": "pnpm test:i18n && pnpm test:thumbnail", "type-check:components": "pnpm --filter @file-viewer/web type-check && pnpm --filter @file-viewer/react type-check && pnpm --filter @file-viewer/react-legacy type-check && pnpm --filter @file-viewer/vue3 type-check && pnpm --filter @file-viewer/vue2.7 type-check && pnpm --filter @file-viewer/vue2.6 type-check && pnpm --filter @file-viewer/jquery type-check && pnpm --filter @file-viewer/svelte type-check", diff --git a/packages/components/web-full/package.json b/packages/components/web-full/package.json index b46459e1b..39e7b8211 100644 --- a/packages/components/web-full/package.json +++ b/packages/components/web-full/package.json @@ -81,6 +81,7 @@ "@file-viewer/renderer-ofd": "workspace:2.4.0", "@file-viewer/renderer-pdf": "workspace:2.4.0", "@file-viewer/renderer-presentation": "workspace:2.4.0", + "@file-viewer/renderer-signature": "workspace:2.4.0", "@file-viewer/renderer-spreadsheet": "workspace:2.4.0", "@file-viewer/renderer-text": "workspace:2.4.0", "@file-viewer/renderer-typst": "workspace:2.4.0", diff --git a/packages/presets/all/package.json b/packages/presets/all/package.json index 40a4e70f9..cda3765b4 100644 --- a/packages/presets/all/package.json +++ b/packages/presets/all/package.json @@ -72,6 +72,7 @@ "@file-viewer/renderer-ofd": "workspace:2.4.0", "@file-viewer/renderer-pdf": "workspace:2.4.0", "@file-viewer/renderer-presentation": "workspace:2.4.0", + "@file-viewer/renderer-signature": "workspace:2.4.0", "@file-viewer/renderer-spreadsheet": "workspace:2.4.0", "@file-viewer/renderer-text": "workspace:2.4.0", "@file-viewer/renderer-typst": "workspace:2.4.0", diff --git a/packages/presets/all/src/index.ts b/packages/presets/all/src/index.ts index 7e7b95873..3c4e94afc 100644 --- a/packages/presets/all/src/index.ts +++ b/packages/presets/all/src/index.ts @@ -24,6 +24,7 @@ import { modelRenderer } from '@file-viewer/renderer-3d'; import { ofdRenderer } from '@file-viewer/renderer-ofd'; import { pdfRenderer } from '@file-viewer/renderer-pdf'; import { presentationRenderer } from '@file-viewer/renderer-presentation'; +import { signatureRenderer } from '@file-viewer/renderer-signature'; import { spreadsheetRenderer } from '@file-viewer/renderer-spreadsheet'; import { textRenderer } from '@file-viewer/renderer-text'; import { typstRenderer } from '@file-viewer/renderer-typst'; @@ -61,7 +62,7 @@ export const fileViewerAllRendererPlugin: FileViewerRendererPlugin = { id: 'file-viewer-preset-all', label: 'Flyfish File Viewer full renderer preset', - renderers: [wordRenderer, wordPerfectRenderer, hangulRenderer, iworkRenderer, pdfRenderer, ofdRenderer, presentationRenderer, spreadsheetRenderer, cadRenderer, typstRenderer, drawingRenderer, modelRenderer, archiveRenderer, emailRenderer, ebookRenderer, textRenderer, imageRenderer, mediaRenderer, mindmapRenderer, geoRenderer, dataRenderer, edaRenderer, fileViewerAllRendererPlugin], + renderers: [wordRenderer, wordPerfectRenderer, hangulRenderer, iworkRenderer, pdfRenderer, ofdRenderer, presentationRenderer, spreadsheetRenderer, signatureRenderer, cadRenderer, typstRenderer, drawingRenderer, modelRenderer, archiveRenderer, emailRenderer, ebookRenderer, textRenderer, imageRenderer, mediaRenderer, mindmapRenderer, geoRenderer, dataRenderer, edaRenderer, fileViewerAllRendererPlugin], }; export const fileViewerPresetAll = allRenderers; diff --git a/packages/renderers/signature/LICENSE b/packages/renderers/signature/LICENSE new file mode 100644 index 000000000..59121b29e --- /dev/null +++ b/packages/renderers/signature/LICENSE @@ -0,0 +1,160 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, +made available under the License, as indicated by a copyright notice that is +included in or attached to the work. + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work by +the copyright owner or by an individual or Legal Entity authorized to submit on +behalf of the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent to the +Licensor or its representatives, including but not limited to communication on +electronic mailing lists, source code control systems, and issue tracking systems +that are managed by, or on behalf of, the Licensor for the purpose of discussing +and improving the Work, but excluding communication that is conspicuously marked +or otherwise designated in writing by the copyright owner as "Not a +Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, +non-exclusive, no-charge, royalty-free, irrevocable copyright license to +reproduce, prepare Derivative Works of, publicly display, publicly perform, +sublicense, and distribute the Work and such Derivative Works in Source or +Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, +each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) patent +license to make, have made, use, offer to sell, sell, import, and otherwise +transfer the Work, where such license applies only to those patent claims +licensable by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) with the Work to +which such Contribution(s) was submitted. If You institute patent litigation +against any entity (including a cross-claim or counterclaim in a lawsuit) +alleging that the Work or a Contribution incorporated within the Work +constitutes direct or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate as of the date +such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or +Derivative Works thereof in any medium, with or without modifications, and in +Source or Object form, provided that You meet the following conditions: + +(a) You must give any other recipients of the Work or Derivative Works a copy of +this License; and + +(b) You must cause any modified files to carry prominent notices stating that +You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works that You +distribute, all copyright, patent, trademark, and attribution notices from the +Source form of the Work, excluding those notices that do not pertain to any part +of the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its distribution, then +any Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the Derivative +Works; within the Source form or documentation, if provided along with the +Derivative Works; or, within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents of the NOTICE +file are for informational purposes only and do not modify the License. You may +add Your own attribution notices within Derivative Works that You distribute, +alongside or as an addendum to the NOTICE text from the Work, provided that such +additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any +Contribution intentionally submitted for inclusion in the Work by You to the +Licensor shall be under the terms and conditions of this License, without any +additional terms or conditions. Notwithstanding the above, nothing herein shall +supersede or modify the terms of any separate license agreement you may have +executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, +trademarks, service marks, or product names of the Licensor, except as required +for reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in +writing, Licensor provides the Work (and each Contributor provides its +Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied, including, without limitation, any warranties or +conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any risks +associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in +tort (including negligence), contract, or otherwise, unless required by +applicable law (such as deliberate and grossly negligent acts) or agreed to in +writing, shall any Contributor be liable to You for damages, including any +direct, indirect, special, incidental, or consequential damages of any character +arising as a result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, work stoppage, +computer failure or malfunction, or any and all other commercial damages or +losses), even if such Contributor has been advised of the possibility of such +damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or +Derivative Works thereof, You may choose to offer, and charge a fee for, +acceptance of support, warranty, indemnity, or other liability obligations and/or +rights consistent with this License. However, in accepting such obligations, You +may act only on Your own behalf and on Your sole responsibility, not on behalf of +any other Contributor, and only if You agree to indemnify, defend, and hold each +Contributor harmless for any liability incurred by, or claims asserted against, +such Contributor by reason of your accepting any such warranty or additional +liability. + +END OF TERMS AND CONDITIONS + +Copyright 2026 Flyfish Viewer diff --git a/packages/renderers/signature/README.en.md b/packages/renderers/signature/README.en.md new file mode 100644 index 000000000..a2f1f8544 --- /dev/null +++ b/packages/renderers/signature/README.en.md @@ -0,0 +1,137 @@ +# @file-viewer/renderer-signature + +Optional, browser-local renderer for cryptographic signatures and timestamp containers. It keeps CMS/CAdES/RFC 3161/RFC 5544 support in TypeScript/Web Crypto and adds OpenPGP inspection through an optional rPGP WebAssembly backend. + +## Supported formats + +- CMS / PKCS#7 / CAdES-adjacent containers: `.p7m`, `.p7s`, `.p7b`, `.p7c`, `.pkcs7`, `.cms`, `.cmsc` +- RFC 3161 timestamp requests, responses and tokens: `.tsq`, `.tsr`, `.tst` +- RFC 5544 `TimeStampedData`: `.tsd` +- OpenPGP routing and inspection: `.asc`, `.sig`, `.pgp`, `.gpg` + +The renderer uses content detection as well as extensions. An `.asc` file can therefore be classified as a message, detached signature, cleartext-signed message, public key or private key instead of being treated as a single fixed format. + +ASiC, ERS, PAdES, XMLDSig/XAdES and JWS/JAdES remain outside the current implementation. + +## Optional registration + +```ts +import { signatureRenderer } from '@file-viewer/renderer-signature'; + +const options = { + rendererMode: 'replace', + renderers: [signatureRenderer], +}; +``` + +To preview extracted PDF or literal data, compose the signature renderer with the relevant optional renderer: + +```ts +import { pdfRenderer } from '@file-viewer/renderer-pdf'; +import { signatureRenderer } from '@file-viewer/renderer-signature'; + +const options = { + rendererMode: 'replace', + renderers: [signatureRenderer, pdfRenderer], +}; +``` + +`@file-viewer/preset-all` includes this renderer explicitly, but `@file-viewer/core` does not import it. + +## OpenPGP backend + +OpenPGP is implemented with rPGP (`pgp` crate `0.20.0`) compiled to WebAssembly. The renderer therefore uses the Rust rPGP backend directly; no additional JavaScript cryptography dependency, GnuPG executable, keyserver integration or server-side conversion is required. + +The loading path is deliberately lazy: + +```text +File Viewer starts +→ no rPGP Worker or WASM is loaded +→ an OpenPGP file is routed to renderer-signature +→ ./openpgp/client.js is dynamically imported +→ signature.worker.js is created +→ the Worker dynamically imports rpgp_wrapper.js +→ rpgp_wrapper_bg.wasm is initialized inside the Worker +``` + +The Rust wrapper intentionally exposes only three operations: + +```text +classify_openpgp +inspect_openpgp +verify_detached_signature +``` + +It does not expose the low-level rPGP object model to JavaScript. + +### OpenPGP scope + +The current backend supports classification and bounded inspection of armored or binary OpenPGP messages, detached signatures, public keys, private-key blocks and unencrypted literal-data messages. Detached signatures can be verified with host/user-supplied public keys. Safely bounded unencrypted literal data can be passed to File Viewer's nested renderer mechanism. + +Encrypted messages are inspection-only. Compressed messages are not recursively expanded unless a future implementation can enforce strict decompressed-output limits. Private-key blocks expose only public metadata derived from the key; secret MPIs, private scalars, passphrases and session keys are never returned to TypeScript. + +Signing, key generation, private-key unlocking and message decryption are intentionally not exposed in this phase. + +Host applications may supply public keys with `options.signature.openPgpPublicKeys`. Detached original content continues to use `options.signature.originalContent`. + +## Default OpenPGP resource limits + +The browser client applies conservative defaults before sending work to WASM: + +| Limit | Default | +| --- | ---: | +| input | 32 MiB | +| extracted output | 16 MiB | +| packet count | 4096 | +| nesting depth | 16 | +| user IDs | 128 | +| subkeys | 128 | +| signatures | 256 | + +They can be overridden with `options.signature.openPgpLimits`. The Rust boundary revalidates input size and bounds returned literal data. No untrusted input is intentionally processed on the main UI thread beyond lightweight routing hints. + +## CMS and timestamp behavior + +The existing browser-local CMS/timestamp implementation can inspect CMS `SignedData`, certificate-only containers and CRLs, parse DER and CMS PEM, recognize CAdES `signingCertificateV2`, verify supported RSA/ECDSA signatures using Web Crypto, compare detached `messageDigest` values, inspect RFC 3161 requests/responses/tokens, and inspect RFC 5544 `TimeStampedData` with embedded or external content. + +External RFC 5544 `dataUri` values are displayed as untrusted metadata and are never fetched automatically. + +## Security and validation boundary + +Successful parsing is not signature verification. Successful cryptographic verification is not certificate/key trust, policy compliance, identity assurance, qualified-signature status or legal validity. The issue #206 CMS/timestamp fixture CA is synthetic and deliberately untrusted. + +The OpenPGP Worker never persists keys or file contents, never performs automatic network lookup and can be terminated through the renderer client cleanup path. The first rPGP phase avoids private-key cryptographic operations; this also avoids depending on private-key RSA operations for which upstream RustCrypto/rPGP documentation requires additional side-channel consideration. + +## Building the rPGP WASM backend + +A Rust toolchain is required only when building the optional OpenPGP backend: + +```bash +rustup target add wasm32-unknown-unknown +cargo install wasm-bindgen-cli +pnpm --filter @file-viewer/renderer-signature build:wasm +``` + +`wasm-opt` is optional; when installed, the build script applies `-Oz`. The script generates `rust/Cargo.lock` if it does not exist, then performs a locked release build and reports raw and Brotli WASM size. + +The regular TypeScript type-check does not require Rust: + +```bash +pnpm --filter @file-viewer/renderer-signature type-check +``` + +## Verification + +The repository contains the contributed non-secret CMS/timestamp fixture corpus under `test/fixtures/github-206-contributed`. + +```bash +pnpm --filter @file-viewer/renderer-signature verify:github-206 +pnpm --filter @file-viewer/renderer-signature verify:fixtures +pnpm --filter @file-viewer/renderer-signature verify:openpgp +``` + +`verify:openpgp` checks the lazy Worker/WASM boundary, pinned rPGP configuration, absence of GnuPG invocation, restricted wrapper API and TypeScript content-detection behavior. Runtime rPGP fixture tests require the WASM backend to be built first. + +## Third-party licensing + +The Rust backend uses the permissively licensed rPGP `pgp` crate. See `THIRD_PARTY_NOTICES.md`. diff --git a/packages/renderers/signature/README.md b/packages/renderers/signature/README.md new file mode 100644 index 000000000..a2f1f8544 --- /dev/null +++ b/packages/renderers/signature/README.md @@ -0,0 +1,137 @@ +# @file-viewer/renderer-signature + +Optional, browser-local renderer for cryptographic signatures and timestamp containers. It keeps CMS/CAdES/RFC 3161/RFC 5544 support in TypeScript/Web Crypto and adds OpenPGP inspection through an optional rPGP WebAssembly backend. + +## Supported formats + +- CMS / PKCS#7 / CAdES-adjacent containers: `.p7m`, `.p7s`, `.p7b`, `.p7c`, `.pkcs7`, `.cms`, `.cmsc` +- RFC 3161 timestamp requests, responses and tokens: `.tsq`, `.tsr`, `.tst` +- RFC 5544 `TimeStampedData`: `.tsd` +- OpenPGP routing and inspection: `.asc`, `.sig`, `.pgp`, `.gpg` + +The renderer uses content detection as well as extensions. An `.asc` file can therefore be classified as a message, detached signature, cleartext-signed message, public key or private key instead of being treated as a single fixed format. + +ASiC, ERS, PAdES, XMLDSig/XAdES and JWS/JAdES remain outside the current implementation. + +## Optional registration + +```ts +import { signatureRenderer } from '@file-viewer/renderer-signature'; + +const options = { + rendererMode: 'replace', + renderers: [signatureRenderer], +}; +``` + +To preview extracted PDF or literal data, compose the signature renderer with the relevant optional renderer: + +```ts +import { pdfRenderer } from '@file-viewer/renderer-pdf'; +import { signatureRenderer } from '@file-viewer/renderer-signature'; + +const options = { + rendererMode: 'replace', + renderers: [signatureRenderer, pdfRenderer], +}; +``` + +`@file-viewer/preset-all` includes this renderer explicitly, but `@file-viewer/core` does not import it. + +## OpenPGP backend + +OpenPGP is implemented with rPGP (`pgp` crate `0.20.0`) compiled to WebAssembly. The renderer therefore uses the Rust rPGP backend directly; no additional JavaScript cryptography dependency, GnuPG executable, keyserver integration or server-side conversion is required. + +The loading path is deliberately lazy: + +```text +File Viewer starts +→ no rPGP Worker or WASM is loaded +→ an OpenPGP file is routed to renderer-signature +→ ./openpgp/client.js is dynamically imported +→ signature.worker.js is created +→ the Worker dynamically imports rpgp_wrapper.js +→ rpgp_wrapper_bg.wasm is initialized inside the Worker +``` + +The Rust wrapper intentionally exposes only three operations: + +```text +classify_openpgp +inspect_openpgp +verify_detached_signature +``` + +It does not expose the low-level rPGP object model to JavaScript. + +### OpenPGP scope + +The current backend supports classification and bounded inspection of armored or binary OpenPGP messages, detached signatures, public keys, private-key blocks and unencrypted literal-data messages. Detached signatures can be verified with host/user-supplied public keys. Safely bounded unencrypted literal data can be passed to File Viewer's nested renderer mechanism. + +Encrypted messages are inspection-only. Compressed messages are not recursively expanded unless a future implementation can enforce strict decompressed-output limits. Private-key blocks expose only public metadata derived from the key; secret MPIs, private scalars, passphrases and session keys are never returned to TypeScript. + +Signing, key generation, private-key unlocking and message decryption are intentionally not exposed in this phase. + +Host applications may supply public keys with `options.signature.openPgpPublicKeys`. Detached original content continues to use `options.signature.originalContent`. + +## Default OpenPGP resource limits + +The browser client applies conservative defaults before sending work to WASM: + +| Limit | Default | +| --- | ---: | +| input | 32 MiB | +| extracted output | 16 MiB | +| packet count | 4096 | +| nesting depth | 16 | +| user IDs | 128 | +| subkeys | 128 | +| signatures | 256 | + +They can be overridden with `options.signature.openPgpLimits`. The Rust boundary revalidates input size and bounds returned literal data. No untrusted input is intentionally processed on the main UI thread beyond lightweight routing hints. + +## CMS and timestamp behavior + +The existing browser-local CMS/timestamp implementation can inspect CMS `SignedData`, certificate-only containers and CRLs, parse DER and CMS PEM, recognize CAdES `signingCertificateV2`, verify supported RSA/ECDSA signatures using Web Crypto, compare detached `messageDigest` values, inspect RFC 3161 requests/responses/tokens, and inspect RFC 5544 `TimeStampedData` with embedded or external content. + +External RFC 5544 `dataUri` values are displayed as untrusted metadata and are never fetched automatically. + +## Security and validation boundary + +Successful parsing is not signature verification. Successful cryptographic verification is not certificate/key trust, policy compliance, identity assurance, qualified-signature status or legal validity. The issue #206 CMS/timestamp fixture CA is synthetic and deliberately untrusted. + +The OpenPGP Worker never persists keys or file contents, never performs automatic network lookup and can be terminated through the renderer client cleanup path. The first rPGP phase avoids private-key cryptographic operations; this also avoids depending on private-key RSA operations for which upstream RustCrypto/rPGP documentation requires additional side-channel consideration. + +## Building the rPGP WASM backend + +A Rust toolchain is required only when building the optional OpenPGP backend: + +```bash +rustup target add wasm32-unknown-unknown +cargo install wasm-bindgen-cli +pnpm --filter @file-viewer/renderer-signature build:wasm +``` + +`wasm-opt` is optional; when installed, the build script applies `-Oz`. The script generates `rust/Cargo.lock` if it does not exist, then performs a locked release build and reports raw and Brotli WASM size. + +The regular TypeScript type-check does not require Rust: + +```bash +pnpm --filter @file-viewer/renderer-signature type-check +``` + +## Verification + +The repository contains the contributed non-secret CMS/timestamp fixture corpus under `test/fixtures/github-206-contributed`. + +```bash +pnpm --filter @file-viewer/renderer-signature verify:github-206 +pnpm --filter @file-viewer/renderer-signature verify:fixtures +pnpm --filter @file-viewer/renderer-signature verify:openpgp +``` + +`verify:openpgp` checks the lazy Worker/WASM boundary, pinned rPGP configuration, absence of GnuPG invocation, restricted wrapper API and TypeScript content-detection behavior. Runtime rPGP fixture tests require the WASM backend to be built first. + +## Third-party licensing + +The Rust backend uses the permissively licensed rPGP `pgp` crate. See `THIRD_PARTY_NOTICES.md`. diff --git a/packages/renderers/signature/THIRD_PARTY_NOTICES.md b/packages/renderers/signature/THIRD_PARTY_NOTICES.md new file mode 100644 index 000000000..69b2bcb03 --- /dev/null +++ b/packages/renderers/signature/THIRD_PARTY_NOTICES.md @@ -0,0 +1,6 @@ +# Third-party notices + +The optional OpenPGP backend uses **rPGP / `pgp` 0.20.0** when the WASM artifact is built. +rPGP is licensed under **MIT OR Apache-2.0**. See . + +The exact transitive Rust dependency set is locked by `rust/Cargo.lock` when generated in a Rust-enabled build environment. diff --git a/packages/renderers/signature/package.json b/packages/renderers/signature/package.json new file mode 100644 index 000000000..8aa25b931 --- /dev/null +++ b/packages/renderers/signature/package.json @@ -0,0 +1,79 @@ +{ + "name": "@file-viewer/renderer-signature", + "version": "2.4.0", + "private": false, + "type": "module", + "description": "Optional browser-local CMS, PKCS#7, CAdES, timestamp and OpenPGP renderer for File Viewer.", + "keywords": [ + "file-viewer", + "renderer", + "signature", + "cms", + "pkcs7", + "cades", + "rfc3161", + "timestamp", + "document-preview", + "self-hosted", + "openpgp", + "rpgp", + "wasm", + "web-worker" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "author": { + "name": "Yu Wang", + "email": "admin@flyfish.dev" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/flyfish-dev/file-viewer.git", + "directory": "packages/renderers/signature" + }, + "homepage": "https://doc.file-viewer.app/guide/on-demand-renderers", + "bugs": { + "url": "https://github.com/flyfish-dev/file-viewer/issues" + }, + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./package.json": "./package.json", + "./worker/signature.worker.js": "./dist/signature.worker.js", + "./wasm/rpgp_wrapper.js": "./dist/rpgp-wasm/rpgp_wrapper.js", + "./wasm/rpgp_wrapper_bg.wasm": "./dist/rpgp-wasm/rpgp_wrapper_bg.wasm" + }, + "files": [ + "dist", + "README.md", + "README.en.md", + "LICENSE", + "THIRD_PARTY_NOTICES.md" + ], + "scripts": { + "build:ts": "tsc -b tsconfig.json --force", + "build:wasm": "node scripts/build-wasm.mjs", + "build": "pnpm build:ts && pnpm build:wasm", + "type-check": "tsc -b tsconfig.json", + "verify:github-206": "pnpm build:ts && node scripts/verify-github-206.mjs", + "verify:fixtures": "bash test/fixtures/github-206-contributed/scripts/verify_all.sh", + "verify:openpgp-architecture": "node scripts/verify-openpgp-architecture.mjs", + "verify:openpgp-detection": "pnpm build:ts && node scripts/verify-openpgp-detection.mjs", + "verify:openpgp": "pnpm verify:openpgp-architecture && pnpm verify:openpgp-detection" + }, + "dependencies": { + "@file-viewer/core": "workspace:2.4.0" + }, + "devDependencies": { + "typescript": "^6.0.3" + }, + "license": "Apache-2.0" +} diff --git a/packages/renderers/signature/rust/Cargo.toml b/packages/renderers/signature/rust/Cargo.toml new file mode 100644 index 000000000..4154976d0 --- /dev/null +++ b/packages/renderers/signature/rust/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "file-viewer-rpgp-wrapper" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +pgp = { version = "=0.20.0", default-features = false, features = ["wasm"] } +serde = { version = "1", features = ["derive"] } +serde-wasm-bindgen = "0.6" +wasm-bindgen = "0.2" diff --git a/packages/renderers/signature/rust/src/classify.rs b/packages/renderers/signature/rust/src/classify.rs new file mode 100644 index 000000000..537d5d7b2 --- /dev/null +++ b/packages/renderers/signature/rust/src/classify.rs @@ -0,0 +1,13 @@ +pub fn armor_type(input: &[u8]) -> Option<&'static str> { + let head = std::str::from_utf8(&input[..input.len().min(512)]).ok()?.trim_start(); + if head.starts_with("-----BEGIN PGP MESSAGE-----") { Some("message") } + else if head.starts_with("-----BEGIN PGP SIGNATURE-----") { Some("signature") } + else if head.starts_with("-----BEGIN PGP SIGNED MESSAGE-----") { Some("cleartext-signed-message") } + else if head.starts_with("-----BEGIN PGP PUBLIC KEY BLOCK-----") { Some("public-key") } + else if head.starts_with("-----BEGIN PGP PRIVATE KEY BLOCK-----") { Some("private-key") } + else { None } +} + +pub fn likely_binary_openpgp(input: &[u8]) -> bool { + input.first().is_some_and(|byte| byte & 0x80 != 0) +} diff --git a/packages/renderers/signature/rust/src/errors.rs b/packages/renderers/signature/rust/src/errors.rs new file mode 100644 index 000000000..d4eedd997 --- /dev/null +++ b/packages/renderers/signature/rust/src/errors.rs @@ -0,0 +1,14 @@ +use serde::Serialize; +use wasm_bindgen::JsValue; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct JsWrapperError<'a> { + code: &'a str, + message: String, +} + +pub fn js_error(code: &'static str, message: impl Into) -> JsValue { + serde_wasm_bindgen::to_value(&JsWrapperError { code, message: message.into() }) + .unwrap_or_else(|_| JsValue::from_str(code)) +} diff --git a/packages/renderers/signature/rust/src/inspect.rs b/packages/renderers/signature/rust/src/inspect.rs new file mode 100644 index 000000000..fb47652c4 --- /dev/null +++ b/packages/renderers/signature/rust/src/inspect.rs @@ -0,0 +1,152 @@ +use std::io::Cursor; + +use pgp::composed::{Deserializable, DetachedSignature, Message, SignedPublicKey, SignedSecretKey}; +use pgp::types::KeyDetails as _; + +use crate::classify::{armor_type, likely_binary_openpgp}; +use crate::errors::js_error; +use crate::types::{InspectionResult, KeySummary, LiteralData, ParseLimits, SubkeySummary}; +use wasm_bindgen::JsValue; + +fn key_summary(key: &SignedPublicKey, kind: &str, limits: &ParseLimits) -> KeySummary { + let user_ids = key.details.users.iter() + .take(limits.max_user_ids) + .map(|user| String::from_utf8_lossy(user.id.id()).into_owned()) + .collect(); + let subkeys = key.public_subkeys.iter().take(limits.max_subkeys).map(|subkey| SubkeySummary { + fingerprint: Some(subkey.fingerprint().to_string()), + key_id: Some(subkey.legacy_key_id().to_string()), + algorithm: Some(format!("{:?}", subkey.algorithm())), + created_at: Some(subkey.created_at().as_secs().to_string()), + }).collect(); + KeySummary { + kind: kind.to_owned(), + version: Some(format!("{:?}", key.version())), + fingerprint: Some(key.fingerprint().to_string()), + key_id: Some(key.legacy_key_id().to_string()), + algorithm: Some(format!("{:?}", key.algorithm())), + created_at: Some(key.created_at().as_secs().to_string()), + user_ids, + subkeys, + } +} + +fn public_keys(input: &[u8], limits: &ParseLimits) -> Vec { + let Ok((iter, _headers)) = SignedPublicKey::from_reader_many(Cursor::new(input.to_vec())) else { + return Vec::new(); + }; + iter.filter_map(Result::ok) + .take(limits.max_subkeys.max(1)) + .map(|key| key_summary(&key, "public", limits)) + .collect() +} + +fn secret_keys(input: &[u8], limits: &ParseLimits) -> Vec { + let Ok((iter, _headers)) = SignedSecretKey::from_reader_many(Cursor::new(input.to_vec())) else { + return Vec::new(); + }; + iter.filter_map(Result::ok) + .take(limits.max_subkeys.max(1)) + .map(|key| key_summary(&key.to_public_key(), "private", limits)) + .collect() +} + +fn detached_signature(input: &[u8]) -> bool { + DetachedSignature::from_reader_single(Cursor::new(input.to_vec())).is_ok() +} + +pub fn inspect(input: &[u8], limits: &ParseLimits, include_literal: bool) -> Result { + let armor = armor_type(input); + let armored = armor.is_some(); + + let public = public_keys(input, limits); + if !public.is_empty() { + return Ok(InspectionResult { + classification: "public-key".into(), armored, armor_type: armor.map(str::to_owned), + packet_types: vec!["public-key".into()], packet_count: public.len(), encrypted: false, + integrity_protected: None, compressed: false, keys: public, signatures: vec![], recipients: vec![], + literal_data: None, warnings: vec![], + }); + } + + let private = secret_keys(input, limits); + if !private.is_empty() { + return Ok(InspectionResult { + classification: "private-key".into(), armored, armor_type: armor.map(str::to_owned), + packet_types: vec!["secret-key".into()], packet_count: private.len(), encrypted: false, + integrity_protected: None, compressed: false, keys: private, signatures: vec![], recipients: vec![], + literal_data: None, + warnings: vec!["Private-key material was parsed only to derive public metadata; secret fields are never returned to JavaScript.".into()], + }); + } + + if detached_signature(input) { + return Ok(InspectionResult { + classification: "detached-signature".into(), armored, armor_type: armor.map(str::to_owned), + packet_types: vec!["signature".into()], packet_count: 1, encrypted: false, + integrity_protected: None, compressed: false, keys: vec![], signatures: vec![], recipients: vec![], + literal_data: None, warnings: vec!["Supply original content and a public key to verify this detached signature.".into()], + }); + } + + if let Ok((mut message, _headers)) = Message::from_reader(Cursor::new(input.to_vec())) { + let encrypted = message.is_encrypted(); + let compressed = message.is_compressed(); + let signed = message.is_signed() || message.is_one_pass_signed(); + let literal = message.is_literal(); + let classification = if encrypted { "encrypted-message" } + else if compressed { "compressed-data" } + else if literal { "literal-data" } + else if signed { "message" } + else { "message" }; + let mut warnings = Vec::new(); + let literal_data = if include_literal && literal { + let header = message.literal_data_header().cloned(); + let mut data = Vec::new(); + use std::io::Read as _; + let read_limit = (limits.max_output_bytes as u64).saturating_add(1); + message.take(read_limit).read_to_end(&mut data) + .map_err(|error| js_error("internal-parser-error", error.to_string()))?; + if data.len() > limits.max_output_bytes { + return Err(js_error("output-too-large", "Literal data exceeds the configured output limit.")); + } + Some(LiteralData { + filename: header + .as_ref() + .map(|value| String::from_utf8_lossy(value.file_name().as_ref()).into_owned()) + .filter(|value| !value.is_empty()), + format: None, + media_type: None, + data, + }) + } else { + if compressed { warnings.push("Compressed OpenPGP content is not automatically decompressed in the first phase.".into()); } + if encrypted { warnings.push("Encrypted OpenPGP content is inspection-only; private-key decryption is not supported.".into()); } + None + }; + return Ok(InspectionResult { + classification: classification.into(), armored, armor_type: armor.map(str::to_owned), + packet_types: vec![classification.into()], packet_count: 1, encrypted, + integrity_protected: None, compressed, keys: vec![], signatures: vec![], recipients: vec![], + literal_data, warnings, + }); + } + + if armor == Some("cleartext-signed-message") { + return Ok(InspectionResult { + classification: "cleartext-signed-message".into(), armored: true, + armor_type: armor.map(str::to_owned), packet_types: vec!["cleartext-signature".into()], packet_count: 1, + encrypted: false, integrity_protected: None, compressed: false, keys: vec![], signatures: vec![], + recipients: vec![], literal_data: None, + warnings: vec!["Cleartext signed messages are classified in this phase; detached public-key verification is the first verification path.".into()], + }); + } + + let classification = if armored || likely_binary_openpgp(input) { "unknown-openpgp" } else { "invalid-openpgp" }; + Ok(InspectionResult { + classification: classification.into(), armored, armor_type: armor.map(str::to_owned), + packet_types: vec![], packet_count: 0, encrypted: false, integrity_protected: None, + compressed: false, keys: vec![], signatures: vec![], recipients: vec![], literal_data: None, + warnings: vec!["The input could not be mapped to a supported composed rPGP object.".into()], + }) +} diff --git a/packages/renderers/signature/rust/src/lib.rs b/packages/renderers/signature/rust/src/lib.rs new file mode 100644 index 000000000..bdb007c96 --- /dev/null +++ b/packages/renderers/signature/rust/src/lib.rs @@ -0,0 +1,88 @@ +mod classify; +mod errors; +mod inspect; +mod limits; +mod types; + +use std::io::Cursor; + +use pgp::composed::{Deserializable, DetachedSignature, SignedPublicKey}; +use pgp::types::KeyDetails as _; +use wasm_bindgen::prelude::*; + +use crate::errors::js_error; +use crate::limits::validate_input; +use crate::types::{ParseLimits, VerificationResult}; + +fn parse_limits(value: JsValue) -> Result { + serde_wasm_bindgen::from_value(value) + .map_err(|error| js_error("invalid-input", format!("Invalid parse limits: {error}"))) +} + +fn to_js(value: &T) -> Result { + serde_wasm_bindgen::to_value(value) + .map_err(|error| js_error("internal-parser-error", format!("Serialization failed: {error}"))) +} + +#[wasm_bindgen] +pub fn classify_openpgp(input: &[u8], limits: JsValue) -> Result { + let limits = parse_limits(limits)?; + validate_input(input, &limits)?; + to_js(&inspect::inspect(input, &limits, false)?) +} + +#[wasm_bindgen] +pub fn inspect_openpgp(input: &[u8], limits: JsValue) -> Result { + let limits = parse_limits(limits)?; + validate_input(input, &limits)?; + to_js(&inspect::inspect(input, &limits, true)?) +} + +#[wasm_bindgen] +pub fn verify_detached_signature( + content: &[u8], + signature: &[u8], + public_keys: JsValue, + limits: JsValue, +) -> Result { + let limits = parse_limits(limits)?; + validate_input(signature, &limits)?; + if content.len() > limits.max_input_bytes { + return Err(js_error("input-too-large", "Original content exceeds the configured input limit.")); + } + let keys: Vec> = serde_wasm_bindgen::from_value(public_keys) + .map_err(|error| js_error("invalid-input", format!("Invalid public key list: {error}")))?; + if keys.is_empty() { + return to_js(&VerificationResult { + status: "public-key-required".into(), valid: None, + key_fingerprint: None, key_id: None, error: None, + }); + } + + let (detached, _) = DetachedSignature::from_reader_single(Cursor::new(signature.to_vec())) + .map_err(|error| js_error("malformed-packet", format!("Detached signature parsing failed: {error}")))?; + + let mut last_error = None; + for key_bytes in keys.into_iter().take(limits.max_subkeys.max(1)) { + let Ok((iter, _headers)) = SignedPublicKey::from_reader_many(Cursor::new(key_bytes)) else { + continue; + }; + for key in iter.filter_map(Result::ok).take(limits.max_subkeys.max(1)) { + match detached.verify(&key, content) { + Ok(()) => { + return to_js(&VerificationResult { + status: "signature-valid".into(), valid: Some(true), + key_fingerprint: Some(key.fingerprint().to_string()), + key_id: Some(key.legacy_key_id().to_string()), error: None, + }); + } + Err(error) => last_error = Some(error.to_string()), + } + } + } + + to_js(&VerificationResult { + status: "signature-invalid".into(), valid: Some(false), + key_fingerprint: None, key_id: None, error: last_error, + }) +} diff --git a/packages/renderers/signature/rust/src/limits.rs b/packages/renderers/signature/rust/src/limits.rs new file mode 100644 index 000000000..ac484bab6 --- /dev/null +++ b/packages/renderers/signature/rust/src/limits.rs @@ -0,0 +1,16 @@ +use crate::errors::js_error; +use crate::types::ParseLimits; +use wasm_bindgen::JsValue; + +pub fn validate_input(input: &[u8], limits: &ParseLimits) -> Result<(), JsValue> { + if input.is_empty() { + return Err(js_error("invalid-input", "OpenPGP input is empty.")); + } + if input.len() > limits.max_input_bytes { + return Err(js_error( + "input-too-large", + format!("OpenPGP input is {} bytes; limit is {} bytes.", input.len(), limits.max_input_bytes), + )); + } + Ok(()) +} diff --git a/packages/renderers/signature/rust/src/types.rs b/packages/renderers/signature/rust/src/types.rs new file mode 100644 index 000000000..356db300a --- /dev/null +++ b/packages/renderers/signature/rust/src/types.rs @@ -0,0 +1,84 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ParseLimits { + pub max_input_bytes: usize, + pub max_output_bytes: usize, + pub max_packet_count: usize, + pub max_nesting_depth: usize, + pub max_user_ids: usize, + pub max_subkeys: usize, + pub max_signatures: usize, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KeySummary { + pub kind: String, + pub version: Option, + pub fingerprint: Option, + pub key_id: Option, + pub algorithm: Option, + pub created_at: Option, + pub user_ids: Vec, + pub subkeys: Vec, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SubkeySummary { + pub fingerprint: Option, + pub key_id: Option, + pub algorithm: Option, + pub created_at: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SignatureSummary { + pub signature_type: Option, + pub hash_algorithm: Option, + pub public_key_algorithm: Option, + pub created_at: Option, + pub expires_at: Option, + pub issuer_key_ids: Vec, + pub issuer_fingerprints: Vec, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LiteralData { + pub filename: Option, + pub format: Option, + pub media_type: Option, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InspectionResult { + pub classification: String, + pub armored: bool, + pub armor_type: Option, + pub packet_types: Vec, + pub packet_count: usize, + pub encrypted: bool, + pub integrity_protected: Option, + pub compressed: bool, + pub keys: Vec, + pub signatures: Vec, + pub recipients: Vec, + pub literal_data: Option, + pub warnings: Vec, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VerificationResult { + pub status: String, + pub valid: Option, + pub key_fingerprint: Option, + pub key_id: Option, + pub error: Option, +} diff --git a/packages/renderers/signature/scripts/build-wasm.mjs b/packages/renderers/signature/scripts/build-wasm.mjs new file mode 100644 index 000000000..6411e4c8f --- /dev/null +++ b/packages/renderers/signature/scripts/build-wasm.mjs @@ -0,0 +1,47 @@ +import { brotliCompressSync, constants as zlibConstants } from 'node:zlib'; +import { execFileSync } from 'node:child_process'; +import { access, mkdir, readFile, rm } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const rustDir = resolve(packageDir, 'rust'); +const targetDir = resolve(rustDir, 'target/wasm32-unknown-unknown/release'); +const outDir = resolve(packageDir, 'dist/rpgp-wasm'); +const wasmInput = resolve(targetDir, 'file_viewer_rpgp_wrapper.wasm'); + +const run = (command, args, options = {}) => { + try { + execFileSync(command, args, { cwd: rustDir, stdio: 'inherit', ...options }); + } catch (error) { + if (error?.code === 'ENOENT') { + throw new Error(`${command} is required to build the optional rPGP WASM backend.`); + } + throw error; + } +}; + +await rm(outDir, { recursive: true, force: true }); +await mkdir(outDir, { recursive: true }); +try { + await access(resolve(rustDir, 'Cargo.lock')); +} catch { + console.warn('[renderer-signature] Cargo.lock is missing; generating it before the locked build.'); + run('cargo', ['generate-lockfile']); +} +run('cargo', ['build', '--release', '--locked', '--target', 'wasm32-unknown-unknown']); +run('wasm-bindgen', [wasmInput, '--target', 'web', '--out-dir', outDir, '--out-name', 'rpgp_wrapper']); + +const wasmOutput = resolve(outDir, 'rpgp_wrapper_bg.wasm'); +try { + execFileSync('wasm-opt', ['-Oz', wasmOutput, '-o', wasmOutput], { stdio: 'inherit' }); +} catch (error) { + if (error?.code !== 'ENOENT') throw error; + console.warn('[renderer-signature] wasm-opt not found; keeping the release wasm-bindgen output.'); +} + +const raw = await readFile(wasmOutput); +const brotli = brotliCompressSync(raw, { + params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 11 }, +}); +console.log(`[renderer-signature] rPGP WASM: ${raw.byteLength} bytes raw, ${brotli.byteLength} bytes Brotli.`); diff --git a/packages/renderers/signature/scripts/verify-github-206.mjs b/packages/renderers/signature/scripts/verify-github-206.mjs new file mode 100644 index 000000000..f61d3bde4 --- /dev/null +++ b/packages/renderers/signature/scripts/verify-github-206.mjs @@ -0,0 +1,153 @@ +import assert from 'node:assert/strict'; +import { readFile, readdir } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { inspectSignatureContainer } from '../dist/signatureAsn1.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const fixtures = resolve(here, '../test/fixtures/github-206-contributed'); +const read = name => readFile(resolve(fixtures, name)); +const inspect = async (name, original) => inspectSignatureContainer(await read(name), original ? { originalContent: await read(original), sourceFilename: name } : { sourceFilename: name }); + +const required = [ + 'cms/invoice-encapsulated.pdf.p7m', + 'cms/invoice-cades-bes.pdf.p7m', + 'cms/invoice-detached.pdf.p7s', + 'cms/xml-encapsulated-but-named.p7s', + 'cms/hello-multi-signer.p7m', + 'cms/invoice-double-signed.p7m', + 'cms/invoice-encapsulated.pem.pkcs7', + 'cms/certificate-chain.p7b', + 'cms/certificate-chain-with-crl.p7c', + 'cms/certificate-only-misnamed.p7m', + 'timestamps/invoice-sha256.tsq', + 'timestamps/invoice-sha256.tsr', + 'timestamps/invoice-sha256.tst', + 'timestamps/hello-sha384-no-nonce.tsq', + 'timestamps/hello-sha384-no-nonce.tsr', + 'timestamps/hello-sha384-no-nonce.tst', + 'timestamps/invoice-embedded.tsd', + 'timestamps/invoice-external-content.tsd', + 'timestamps/rejected-bad-alg.tsr', + 'negative/truncated-signed-data.p7m', + 'negative/plain-text-with-p7s-extension.p7s', +]; +for (const name of required) await read(name); + +const invoice = await read('originals/invoice.pdf'); +const encapsulated = await inspect('cms/invoice-encapsulated.pdf.p7m'); +assert.equal(encapsulated.kind, 'cms-signed-data'); +assert.deepEqual(Buffer.from(encapsulated.embeddedContent || []), invoice); +assert.equal(encapsulated.signers.length, 1); +assert.equal(encapsulated.signers[0]?.digestMatches, true); +assert.equal(encapsulated.signers[0]?.cryptographicSignatureValid, true); + +const cades = await inspect('cms/invoice-cades-bes.pdf.p7m'); +assert.equal(cades.signers[0]?.signingCertificateV2, true); +assert.deepEqual(Buffer.from(cades.embeddedContent || []), invoice); + +const detached = await inspect('cms/invoice-detached.pdf.p7s', 'originals/invoice.pdf'); +assert.equal(detached.detached, true); +assert.equal(detached.signers[0]?.digestMatches, true); +assert.equal(detached.signers[0]?.cryptographicSignatureValid, true); +const detachedBad = await inspect('cms/invoice-detached.pdf.p7s', 'originals/invoice-tampered.pdf'); +assert.equal(detachedBad.signers[0]?.digestMatches, false); + +const p7sEmbedded = await inspect('cms/xml-encapsulated-but-named.p7s'); +assert.equal(p7sEmbedded.detached, false); +assert.ok((p7sEmbedded.embeddedContent?.byteLength || 0) > 0); + +const multi = await inspect('cms/hello-multi-signer.p7m'); +assert.equal(multi.signers.length, 2); +assert.equal(multi.signers.every(signer => signer.cryptographicSignatureValid === true), true); +assert.ok(multi.signers.some(signer => /RSA/i.test(signer.signatureAlgorithm || ''))); +assert.ok(multi.signers.some(signer => /ECDSA/i.test(signer.signatureAlgorithm || ''))); + +const outer = await inspect('cms/invoice-double-signed.p7m'); +assert.equal(outer.kind, 'cms-signed-data'); +assert.ok(outer.embeddedContent); +const inner = await inspectSignatureContainer(outer.embeddedContent); +assert.equal(inner.kind, 'cms-signed-data'); +assert.deepEqual(Buffer.from(inner.embeddedContent || []), invoice); + +const pem = await inspect('cms/invoice-encapsulated.pem.pkcs7'); +assert.equal(pem.kind, 'cms-signed-data'); +assert.equal(pem.signers.length, 1); + +const p7b = await inspect('cms/certificate-chain.p7b'); +assert.equal(p7b.kind, 'cms-certificates'); +assert.equal(p7b.certificates.length, 4); +assert.equal(p7b.crlCount, 0); +const p7c = await inspect('cms/certificate-chain-with-crl.p7c'); +assert.equal(p7c.kind, 'cms-certificates'); +assert.equal(p7c.certificates.length, 4); +assert.equal(p7c.crlCount, 1); +const misnamed = await inspect('cms/certificate-only-misnamed.p7m'); +assert.equal(misnamed.kind, 'cms-certificates'); +assert.equal(misnamed.signers.length, 0); + +const tsq256 = await inspect('timestamps/invoice-sha256.tsq', 'originals/invoice.pdf'); +assert.equal(tsq256.kind, 'timestamp-request'); +assert.equal(tsq256.timestamp?.messageImprintAlgorithm, 'SHA-256'); +assert.equal(tsq256.timestamp?.messageImprintMatchesOriginal, true); +assert.ok(tsq256.timestamp?.nonce); +assert.equal(tsq256.timestamp?.certReq, true); + +const tsr256 = await inspect('timestamps/invoice-sha256.tsr', 'originals/invoice.pdf'); +assert.equal(tsr256.kind, 'timestamp-response'); +assert.equal(tsr256.timestampResponse?.statusLabel, 'granted'); +assert.equal(tsr256.timestamp?.messageImprintMatchesOriginal, true); +const tst256 = await inspect('timestamps/invoice-sha256.tst', 'originals/invoice.pdf'); +assert.equal(tst256.kind, 'timestamp-token'); +assert.equal(tst256.timestamp?.messageImprintMatchesOriginal, true); + +const tsq384 = await inspect('timestamps/hello-sha384-no-nonce.tsq', 'originals/hello.txt'); +assert.equal(tsq384.timestamp?.messageImprintAlgorithm, 'SHA-384'); +assert.equal(tsq384.timestamp?.nonce, undefined); +assert.equal(tsq384.timestamp?.certReq, true); +const tsr384 = await inspect('timestamps/hello-sha384-no-nonce.tsr', 'originals/hello.txt'); +assert.equal(tsr384.timestamp?.messageImprintAlgorithm, 'SHA-384'); +assert.equal(tsr384.timestamp?.messageImprintMatchesOriginal, true); +const tst384 = await inspect('timestamps/hello-sha384-no-nonce.tst', 'originals/hello.txt'); +assert.equal(tst384.timestamp?.messageImprintAlgorithm, 'SHA-384'); +assert.equal(tst384.timestamp?.messageImprintMatchesOriginal, true); + +const rejected = await inspect('timestamps/rejected-bad-alg.tsr'); +assert.equal(rejected.kind, 'timestamp-response'); +assert.equal(rejected.timestampResponse?.statusLabel, 'rejection'); +assert.match(rejected.timestampResponse?.failureInfo || '', /badAlg/); + +const tsdEmbedded = await inspect('timestamps/invoice-embedded.tsd'); +assert.equal(tsdEmbedded.kind, 'timestamped-data'); +assert.deepEqual(Buffer.from(tsdEmbedded.embeddedContent || []), invoice); +assert.equal(tsdEmbedded.timestampedData?.filename, 'invoice.pdf'); +assert.equal(tsdEmbedded.timestampedData?.mediaType, 'application/pdf'); +assert.equal(tsdEmbedded.timestamp?.messageImprintMatchesOriginal, true); +const tsdExternal = await inspect('timestamps/invoice-external-content.tsd', 'originals/invoice.pdf'); +assert.equal(tsdExternal.kind, 'timestamped-data'); +assert.equal(tsdExternal.embeddedContent, undefined); +assert.match(tsdExternal.timestampedData?.dataUri || '', /^https:\/\/example\.invalid\//); +assert.equal(tsdExternal.timestamp?.messageImprintMatchesOriginal, true); + +await assert.rejects(() => inspect('negative/truncated-signed-data.p7m')); +await assert.rejects(() => inspect('negative/plain-text-with-p7s-extension.p7s')); + +const packageJson = JSON.parse(await readFile(resolve(here, '../package.json'), 'utf8')); +const dependencyNames = Object.keys({ ...packageJson.dependencies, ...packageJson.optionalDependencies, ...packageJson.peerDependencies }); +assert.equal(dependencyNames.some(name => /openpgp|gnupg|gpg/i.test(name)), false, 'Phase one must not introduce OpenPGP dependencies.'); + +const allFiles = async dir => { + const result = []; + for (const entry of await readdir(dir, { withFileTypes: true })) { + const path = resolve(dir, entry.name); + if (entry.isDirectory()) result.push(...await allFiles(path)); + else result.push(path); + } + return result; +}; +for (const path of await allFiles(fixtures)) { + const data = await readFile(path); + assert.equal(/-----BEGIN (?:RSA |EC |OPENSSH |ENCRYPTED )?PRIVATE KEY-----/.test(data.toString('utf8')), false, `Private key marker found in ${path}`); +} + +console.log('GitHub issue #206 contributed CMS/RFC 3161/RFC 5544 fixture verification passed.'); diff --git a/packages/renderers/signature/scripts/verify-openpgp-architecture.mjs b/packages/renderers/signature/scripts/verify-openpgp-architecture.mjs new file mode 100644 index 000000000..8d53af56c --- /dev/null +++ b/packages/renderers/signature/scripts/verify-openpgp-architecture.mjs @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict'; +import { readFile, readdir } from 'node:fs/promises'; +import { dirname, extname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const read = path => readFile(resolve(packageDir, path), 'utf8'); + +const cargo = await read('rust/Cargo.toml'); +assert.match(cargo, /pgp\s*=\s*\{[^}]*version\s*=\s*"=0\.20\.0"[^}]*features\s*=\s*\["wasm"\]/s, 'rPGP 0.20.0 with the wasm feature must be pinned.'); +assert.doesNotMatch(cargo, /lgpl/i, 'The Rust manifest must not introduce LGPL code.'); + +const worker = await read('src/signature.worker.ts'); +assert.match(worker, /import\([^)]*moduleUrl[^)]*\)/, 'The Worker must lazily import the wasm-bindgen module.'); +assert.match(worker, /rpgp_wrapper_bg\.wasm/, 'The Worker must initialize the standalone WASM asset.'); +assert.doesNotMatch(worker, /gnupg|gpg\s+--/i, 'The Worker must not invoke GnuPG.'); + +const client = await read('src/openpgp/client.ts'); +assert.match(client, /new Worker\(/, 'The OpenPGP backend must execute in a Web Worker.'); +assert.match(client, /\.\.\/signature\.worker\.js/, 'The Worker must be created from the emitted optional renderer asset.'); + +const rendererIndex = await read('src/index.ts'); +for (const extension of ['asc', 'sig', 'pgp', 'gpg']) { + assert.match(rendererIndex, new RegExp(`['\"]${extension}['\"]`), `${extension} must be registered by the optional signature renderer.`); +} + +const protocol = await read('src/workerProtocol.ts'); +for (const operation of ['classify', 'inspect', 'verify-detached']) { + assert.match(protocol, new RegExp(`type:\\s*['\"]${operation}['\"]`), `Worker protocol must include ${operation}.`); +} +for (const forbidden of ['decrypt', 'sign-message', 'generate-key', 'unlock-private-key']) { + assert.doesNotMatch(protocol, new RegExp(`type:\\s*['\"]${forbidden}['\"]`), `Sensitive operation ${forbidden} must not be exposed.`); +} + +const rustLib = await read('rust/src/lib.rs'); +for (const exported of ['classify_openpgp', 'inspect_openpgp', 'verify_detached_signature']) { + assert.match(rustLib, new RegExp(`pub fn ${exported}\\b`), `Rust wrapper must export ${exported}.`); +} +assert.doesNotMatch(rustLib, /pub fn (?:decrypt|sign|generate_key|unlock_private)/, 'Rust wrapper must not expose private-key operations.'); + +async function collectSourceFiles(directory) { + const result = []; + for (const entry of await readdir(directory, { withFileTypes: true })) { + if (['dist', 'target', 'test'].includes(entry.name)) continue; + const path = join(directory, entry.name); + if (entry.isDirectory()) result.push(...await collectSourceFiles(path)); + else if (['.ts', '.js', '.mjs', '.rs', '.json', '.toml'].includes(extname(entry.name))) result.push(path); + } + return result; +} + +for (const file of await collectSourceFiles(packageDir)) { + if (file === fileURLToPath(import.meta.url)) continue; + const text = await readFile(file, 'utf8'); + assert.doesNotMatch(text, /child_process[^\n]*(?:gpg|gnupg)|(?:exec|spawn)[^(]*\([^\n]*(?:gpg|gnupg)/i, `External GnuPG invocation remains in ${file}.`); +} + +console.log('OpenPGP rPGP/WASM architecture verification passed.'); diff --git a/packages/renderers/signature/scripts/verify-openpgp-detection.mjs b/packages/renderers/signature/scripts/verify-openpgp-detection.mjs new file mode 100644 index 000000000..8cb647a76 --- /dev/null +++ b/packages/renderers/signature/scripts/verify-openpgp-detection.mjs @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import { + detectOpenPgpArmorType, + hasPlausibleOpenPgpPacketHeader, + isProbablyOpenPgp, +} from '../dist/openpgp/formatDetection.js'; + +const enc = new TextEncoder(); +const cases = [ + ['-----BEGIN PGP MESSAGE-----\n', 'message'], + ['-----BEGIN PGP SIGNATURE-----\n', 'signature'], + ['-----BEGIN PGP SIGNED MESSAGE-----\n', 'cleartext-signed-message'], + ['-----BEGIN PGP PUBLIC KEY BLOCK-----\n', 'public-key'], + ['-----BEGIN PGP PRIVATE KEY BLOCK-----\n', 'private-key'], +]; +for (const [source, armorType] of cases) { + const bytes = enc.encode(source); + assert.equal(detectOpenPgpArmorType(bytes), armorType); + assert.equal(isProbablyOpenPgp(bytes), true); +} + +assert.equal(isProbablyOpenPgp(enc.encode('plain text'), 'document.sig'), true, 'Known OpenPGP extensions are routing hints.'); +assert.equal(isProbablyOpenPgp(enc.encode('plain text'), 'document.txt'), false, 'Unrelated plain text must not be classified as OpenPGP.'); +assert.equal(hasPlausibleOpenPgpPacketHeader(Uint8Array.from([0xc8, 0x00])), true, 'Plausible binary packet header must be recognized.'); +console.log('OpenPGP format-detection verification passed.'); diff --git a/packages/renderers/signature/src/index.ts b/packages/renderers/signature/src/index.ts new file mode 100644 index 000000000..9fd912b5f --- /dev/null +++ b/packages/renderers/signature/src/index.ts @@ -0,0 +1,58 @@ +import type { + FileRenderHandler, + FileViewerRenderedInstance, + FileViewerRendererPlugin, + RendererDefinition, +} from '@file-viewer/core'; + +export const signatureRendererDefinition: RendererDefinition = { + id: 'signature', + label: 'Digital signature and timestamp', + category: 'document', + extensions: ['p7m', 'p7s', 'p7b', 'p7c', 'pkcs7', 'cms', 'cmsc', 'tsq', 'tsr', 'tst', 'tsd', 'asc', 'sig', 'pgp', 'gpg'], + async: true, + supportLevel: 'experimental', + status: 'experimental', + packageName: '@file-viewer/renderer-signature', + presets: ['all'], + knownLimits: [ + 'Phase one supports CMS/PKCS#7, selected CAdES attributes, RFC 3161, and RFC 5544 TimeStampedData.', + 'Certificate trust, policy compliance, qualified status, and legal validity are not established.', + 'OpenPGP inspection and detached verification use an optional lazy rPGP WebAssembly Worker; private-key operations remain out of scope.', + 'ASiC, evidence records, PAdES, XAdES, and JAdES remain out of scope.', + ], + capabilities: { download: true }, +}; + +export const renderFileViewerSignature: FileRenderHandler = ( + buffer, + target, + type, + context +) => import('./signature.js').then(({ default: renderSignature }) => renderSignature(buffer, target, type, context)); + +export const signatureRenderer: FileViewerRendererPlugin> = { + id: 'file-viewer-renderer-signature', + label: 'Flyfish File Viewer signature renderer', + definitions: [signatureRendererDefinition], + handlers: [{ rendererId: signatureRendererDefinition.id, handler: renderFileViewerSignature }], +}; + +export { + inspectSignatureContainer, + signatureOidLabels, + type InspectSignatureOptions, + type SignatureCertificateSummary, + type SignatureContainerKind, + type SignatureInspection, + type SignatureSignerSummary, + type TimestampInfoSummary, + type TimestampResponseSummary, +} from './signatureAsn1.js'; +export type { FileViewerSignatureOptions } from './signature.js'; + +export default signatureRenderer; + +export { OpenPgpWorkerClient, DEFAULT_SIGNATURE_PARSE_LIMITS } from './openpgp/client.js'; +export { isProbablyOpenPgp, detectOpenPgpArmorType } from './openpgp/formatDetection.js'; +export type { SignatureParseLimits, OpenPgpInspectionResult, OpenPgpVerificationResult, OpenPgpKeySummary, OpenPgpSignatureSummary, ExtractedLiteralData } from './openpgp/types.js'; diff --git a/packages/renderers/signature/src/openpgp/client.ts b/packages/renderers/signature/src/openpgp/client.ts new file mode 100644 index 000000000..d0028b3d9 --- /dev/null +++ b/packages/renderers/signature/src/openpgp/client.ts @@ -0,0 +1,115 @@ +import type { SignatureWorkerRequest, SignatureWorkerResponse } from '../workerProtocol.js'; +import type { + OpenPgpInspectionResult, + OpenPgpVerificationResult, + SignatureParseLimits, +} from './types.js'; + +export const DEFAULT_SIGNATURE_PARSE_LIMITS: SignatureParseLimits = { + maxInputBytes: 32 * 1024 * 1024, + maxOutputBytes: 16 * 1024 * 1024, + maxPacketCount: 4096, + maxNestingDepth: 16, + maxUserIds: 128, + maxSubkeys: 128, + maxSignatures: 256, +}; + +let nextRequestId = 0; + +const mergeLimits = (limits?: Partial): SignatureParseLimits => ({ + ...DEFAULT_SIGNATURE_PARSE_LIMITS, + ...limits, +}); + +export class OpenPgpWorkerClient { + private worker?: Worker; + private readonly pending = new Map(); + + private ensureWorker() { + if (this.worker) return this.worker; + if (typeof Worker === 'undefined') { + throw new Error('OpenPGP inspection requires Web Worker support.'); + } + const worker = new Worker(new URL('../signature.worker.js', import.meta.url), { + type: 'module', + name: 'file-viewer-signature-openpgp', + }); + worker.addEventListener('message', (event: MessageEvent) => { + const pending = this.pending.get(event.data.id); + if (!pending) return; + this.pending.delete(event.data.id); + if (event.data.ok) pending.resolve(event.data.result); + else pending.reject(Object.assign(new Error(event.data.error.message), { code: event.data.error.code })); + }); + worker.addEventListener('error', event => { + const error = new Error(event.message || 'Signature Worker failed.'); + for (const pending of this.pending.values()) pending.reject(error); + this.pending.clear(); + worker.terminate(); + if (this.worker === worker) this.worker = undefined; + }); + this.worker = worker; + return worker; + } + + private request( + request: Record, + transfers: Transferable[] + ): Promise { + const worker = this.ensureWorker(); + const id = `signature-${++nextRequestId}`; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve: value => resolve(value as T), reject }); + worker.postMessage({ ...request, id } as SignatureWorkerRequest, transfers); + }); + } + + inspect(input: ArrayBuffer, limits?: Partial) { + const transferable = input.slice(0); + return this.request( + { type: 'inspect', input: transferable, limits: mergeLimits(limits) }, + [transferable] + ); + } + + classify(input: ArrayBuffer, limits?: Partial) { + const transferable = input.slice(0); + return this.request( + { type: 'classify', input: transferable, limits: mergeLimits(limits) }, + [transferable] + ); + } + + verifyDetached( + content: ArrayBuffer, + signature: ArrayBuffer, + publicKeys: ArrayBuffer[], + limits?: Partial + ) { + const contentTransfer = content.slice(0); + const signatureTransfer = signature.slice(0); + const keyTransfers = publicKeys.map(key => key.slice(0)); + return this.request( + { + type: 'verify-detached', + content: contentTransfer, + signature: signatureTransfer, + publicKeys: keyTransfers, + limits: mergeLimits(limits), + }, + [contentTransfer, signatureTransfer, ...keyTransfers] + ); + } + + dispose() { + this.worker?.terminate(); + this.worker = undefined; + const error = new DOMException('Signature Worker was terminated.', 'AbortError'); + for (const pending of this.pending.values()) pending.reject(error); + this.pending.clear(); + } +} diff --git a/packages/renderers/signature/src/openpgp/formatDetection.ts b/packages/renderers/signature/src/openpgp/formatDetection.ts new file mode 100644 index 000000000..6dc9755dd --- /dev/null +++ b/packages/renderers/signature/src/openpgp/formatDetection.ts @@ -0,0 +1,42 @@ +const ARMOR_MARKERS = [ + ['-----BEGIN PGP MESSAGE-----', 'message'], + ['-----BEGIN PGP SIGNATURE-----', 'signature'], + ['-----BEGIN PGP SIGNED MESSAGE-----', 'cleartext-signed-message'], + ['-----BEGIN PGP PUBLIC KEY BLOCK-----', 'public-key'], + ['-----BEGIN PGP PRIVATE KEY BLOCK-----', 'private-key'], +] as const; + +const OPENPGP_EXTENSIONS = new Set(['asc', 'sig', 'pgp', 'gpg']); + +const extensionOf = (filename?: string) => { + if (!filename) return ''; + const base = filename.split(/[\\/]/).pop() || filename; + const dot = base.lastIndexOf('.'); + return dot >= 0 ? base.slice(dot + 1).toLowerCase() : ''; +}; + +export const detectOpenPgpArmorType = (bytes: Uint8Array) => { + const head = new TextDecoder('utf-8', { fatal: false }).decode(bytes.subarray(0, Math.min(bytes.byteLength, 512))).trimStart(); + return ARMOR_MARKERS.find(([marker]) => head.startsWith(marker))?.[1]; +}; + +export const hasPlausibleOpenPgpPacketHeader = (bytes: Uint8Array) => { + if (!bytes.byteLength) return false; + const first = bytes[0] ?? 0; + if ((first & 0x80) === 0) return false; + if ((first & 0x40) !== 0) { + const tag = first & 0x3f; + return tag > 0 && tag < 64; + } + const tag = (first >> 2) & 0x0f; + return tag > 0 && tag < 16; +}; + +export const isProbablyOpenPgp = (bytes: Uint8Array, filename?: string, typeHint?: string) => { + if (detectOpenPgpArmorType(bytes)) return true; + const type = (typeHint || '').replace(/^\./, '').toLowerCase(); + if (OPENPGP_EXTENSIONS.has(type) || OPENPGP_EXTENSIONS.has(extensionOf(filename))) { + return true; + } + return hasPlausibleOpenPgpPacketHeader(bytes); +}; diff --git a/packages/renderers/signature/src/openpgp/types.ts b/packages/renderers/signature/src/openpgp/types.ts new file mode 100644 index 000000000..0f5265eaa --- /dev/null +++ b/packages/renderers/signature/src/openpgp/types.ts @@ -0,0 +1,97 @@ +export type OpenPgpClassification = + | 'message' + | 'encrypted-message' + | 'detached-signature' + | 'cleartext-signed-message' + | 'public-key' + | 'private-key' + | 'literal-data' + | 'compressed-data' + | 'unknown-openpgp' + | 'invalid-openpgp'; + +export interface SignatureParseLimits { + maxInputBytes: number; + maxOutputBytes: number; + maxPacketCount: number; + maxNestingDepth: number; + maxUserIds: number; + maxSubkeys: number; + maxSignatures: number; +} + +export interface OpenPgpKeySummary { + kind: 'public' | 'private'; + version?: string; + fingerprint?: string; + keyId?: string; + algorithm?: string; + createdAt?: string; + userIds: string[]; + subkeys: Array<{ + fingerprint?: string; + keyId?: string; + algorithm?: string; + createdAt?: string; + }>; +} + +export interface OpenPgpSignatureSummary { + signatureType?: string; + hashAlgorithm?: string; + publicKeyAlgorithm?: string; + createdAt?: string; + expiresAt?: string; + issuerKeyIds: string[]; + issuerFingerprints: string[]; +} + +export interface ExtractedLiteralData { + filename?: string; + format?: string; + mediaType?: string; + data: Uint8Array; +} + +export interface OpenPgpInspectionResult { + classification: OpenPgpClassification; + armored: boolean; + armorType?: string; + packetTypes: string[]; + packetCount: number; + encrypted: boolean; + integrityProtected?: boolean; + compressed: boolean; + keys: OpenPgpKeySummary[]; + signatures: OpenPgpSignatureSummary[]; + recipients: string[]; + literalData?: ExtractedLiteralData; + warnings: string[]; +} + +export interface OpenPgpVerificationResult { + status: 'signature-valid' | 'signature-invalid' | 'public-key-required' | 'original-content-required' | 'unsupported-algorithm'; + valid?: boolean; + keyFingerprint?: string; + keyId?: string; + error?: string; +} + +export interface OpenPgpWorkerError { + code: + | 'invalid-input' + | 'unsupported-format' + | 'unsupported-algorithm' + | 'input-too-large' + | 'output-too-large' + | 'packet-limit-exceeded' + | 'nesting-limit-exceeded' + | 'decompression-required' + | 'encrypted-content' + | 'public-key-required' + | 'original-content-required' + | 'malformed-packet' + | 'wasm-initialization-failed' + | 'internal-parser-error'; + message: string; +} diff --git a/packages/renderers/signature/src/signature.ts b/packages/renderers/signature/src/signature.ts new file mode 100644 index 000000000..3347f3c17 --- /dev/null +++ b/packages/renderers/signature/src/signature.ts @@ -0,0 +1,731 @@ +import { + disposeFileViewerRendered, + normalizeFileViewerUiDensity, + type FileRenderContext, + type FileViewerOptions, + type FileViewerRenderedInstance, +} from '@file-viewer/core'; +import { + inspectSignatureContainer, + type SignatureCertificateSummary, + type SignatureInspection, + type SignatureSignerSummary, + type TimestampInfoSummary, +} from './signatureAsn1.js'; +import { isProbablyOpenPgp } from './openpgp/formatDetection.js'; +import type { OpenPgpWorkerClient as OpenPgpWorkerClientType } from './openpgp/client.js'; +import type { + OpenPgpInspectionResult, + OpenPgpVerificationResult, + SignatureParseLimits, +} from './openpgp/types.js'; + +const DEFAULT_MAX_CONTAINER_SIZE = 64 * 1024 * 1024; +const DEFAULT_MAX_NESTED_PREVIEW_SIZE = 32 * 1024 * 1024; + +export interface FileViewerSignatureOptions { + /** Original content used for detached CMS signatures and RFC 3161 imprint comparison. */ + originalContent?: ArrayBuffer | Blob; + originalFilename?: string; + /** Maximum cryptographic container size parsed in memory. Defaults to 64 MiB. */ + maxContainerSize?: number; + /** Maximum extracted or supplied original size sent to a nested renderer. Defaults to 32 MiB. */ + maxNestedPreviewSize?: number; + /** Public OpenPGP keys used only for detached-signature verification. Secret keys are not accepted for verification. */ + openPgpPublicKeys?: Array; + /** Conservative parser/resource limits forwarded to the rPGP WebAssembly wrapper. */ + openPgpLimits?: Partial; +} + +type SignatureFileViewerOptions = FileViewerOptions & { + signature?: FileViewerSignatureOptions; +}; + +const signatureStyle = ` +.signature-shell{box-sizing:border-box;width:100%;height:100%;min-width:0;min-height:0;overflow:auto;background:#edf2f7;color:#172033;font-family:Aptos,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif} +.signature-shell *{box-sizing:border-box} +.signature-header{position:sticky;top:0;z-index:3;display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:16px 18px;border-bottom:1px solid rgba(23,32,51,.09);background:rgba(255,255,255,.94);backdrop-filter:blur(12px)} +.signature-title{min-width:0}.signature-title small{display:block;color:#24765a;font-size:11px;font-weight:900;letter-spacing:.08em}.signature-title h2{margin:3px 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:19px}.signature-title p{margin:5px 0 0;color:#64748b;font-size:12px}.signature-phase{flex:0 0 auto;padding:7px 10px;border-radius:999px;background:#e7f8ef;color:#166534;font-size:11px;font-weight:900} +.signature-content{display:grid;grid-template-columns:minmax(260px,340px) minmax(0,1fr);gap:14px;padding:14px} +.signature-sidebar,.signature-main{min-width:0;display:flex;flex-direction:column;gap:12px} +.signature-card{min-width:0;padding:14px;border:1px solid rgba(23,32,51,.08);border-radius:14px;background:#fff;box-shadow:0 8px 24px rgba(23,32,51,.05)} +.signature-card h3{margin:0 0 10px;font-size:14px}.signature-card h4{margin:13px 0 7px;font-size:12px}.signature-card p{margin:6px 0;color:#526174;font-size:12px;line-height:1.55}.signature-card code{font-family:'SFMono-Regular',Consolas,monospace;font-size:11px;overflow-wrap:anywhere} +.signature-grid{display:grid;grid-template-columns:minmax(110px,auto) minmax(0,1fr);gap:7px 10px;margin:0}.signature-grid dt{color:#708095;font-size:11px;font-weight:800}.signature-grid dd{min-width:0;margin:0;overflow-wrap:anywhere;font-size:12px} +.signature-warning{padding:10px 11px;border-radius:10px;background:#fff7e8;color:#854d0e;font-size:11px;line-height:1.5}.signature-warning+.signature-warning{margin-top:7px} +.signature-status-list{display:flex;flex-wrap:wrap;gap:6px}.signature-status{display:inline-flex;align-items:center;gap:5px;padding:5px 8px;border-radius:999px;background:#eef2f7;color:#475569;font-size:10px;font-weight:900}.signature-status[data-state='valid']{background:#dcfce7;color:#166534}.signature-status[data-state='invalid']{background:#fee2e2;color:#991b1b}.signature-status[data-state='pending']{background:#fef3c7;color:#92400e} +.signature-item{padding:11px;border:1px solid rgba(23,32,51,.08);border-radius:11px;background:#f8fafc}.signature-item+.signature-item{margin-top:9px}.signature-item-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.signature-item-head strong{font-size:12px}.signature-item-head span{color:#64748b;font-size:10px} +.signature-original{display:flex;flex-direction:column;gap:9px}.signature-file{width:100%;font-size:12px}.signature-file::file-selector-button{margin-right:9px;padding:7px 10px;border:0;border-radius:8px;background:#1f7a58;color:#fff;font:inherit;font-weight:800;cursor:pointer}.signature-original-state{min-height:18px;color:#64748b;font-size:11px} +.signature-preview{min-height:360px;overflow:hidden;padding:0}.signature-preview-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;border-bottom:1px solid rgba(23,32,51,.08)}.signature-preview-head strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.signature-preview-target{min-height:340px;height:min(65vh,720px);overflow:auto;background:#f8fafc}.signature-preview-empty{height:100%;min-height:340px;display:flex;align-items:center;justify-content:center;padding:28px;color:#64748b;text-align:center;font-size:12px;line-height:1.6} +.signature-openpgp-note{padding:10px 11px;border-radius:10px;background:#eef6ff;color:#1e3a5f;font-size:11px;line-height:1.5}.signature-key-input{display:flex;flex-direction:column;gap:8px} +.signature-error{padding:14px;border:1px solid #fecaca;border-radius:12px;background:#fff1f2;color:#991b1b;font-size:12px;line-height:1.55} +.signature-loading{display:flex;align-items:center;gap:10px;padding:15px;color:#526174;font-size:12px}.signature-spinner{width:20px;height:20px;border:2px solid rgba(31,122,88,.18);border-top-color:#1f7a58;border-radius:999px;animation:signature-spin .8s linear infinite} +.signature-shell[data-viewer-density='compact'] .signature-header{padding:9px 11px}.signature-shell[data-viewer-density='compact'] .signature-content{gap:8px;padding:8px}.signature-shell[data-viewer-density='compact'] .signature-card{padding:10px;border-radius:10px} +[data-viewer-theme='dark'] .signature-shell{background:#111827;color:#e5edf7}[data-viewer-theme='dark'] .signature-header,[data-viewer-theme='dark'] .signature-card{background:#182233;border-color:rgba(255,255,255,.08)}[data-viewer-theme='dark'] .signature-item,[data-viewer-theme='dark'] .signature-preview-target{background:#101827;border-color:rgba(255,255,255,.08)}[data-viewer-theme='dark'] .signature-title p,[data-viewer-theme='dark'] .signature-card p,[data-viewer-theme='dark'] .signature-grid dt,[data-viewer-theme='dark'] .signature-item-head span,[data-viewer-theme='dark'] .signature-original-state,[data-viewer-theme='dark'] .signature-preview-empty{color:#9eabc0} +@media(max-width:860px){.signature-content{grid-template-columns:1fr}.signature-header{position:relative}.signature-preview-target{height:520px}} +@keyframes signature-spin{to{transform:rotate(360deg)}} +`; + +const createElement = ( + documentRef: Document, + tag: K, + className?: string, + text?: string +) => { + const element = documentRef.createElement(tag); + if (className) element.className = className; + if (text !== undefined) element.textContent = text; + return element; +}; + +const formatBytes = (value: number) => { + if (value < 1024) return `${value} B`; + if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KiB`; + return `${(value / (1024 * 1024)).toFixed(1)} MiB`; +}; + +const valueOrDash = (value: unknown) => value === undefined || value === null || value === '' + ? '—' + : String(value); + +const appendDefinitionList = ( + documentRef: Document, + parent: HTMLElement, + rows: Array<[string, unknown]> +) => { + const list = createElement(documentRef, 'dl', 'signature-grid'); + rows.forEach(([label, value]) => { + list.append( + createElement(documentRef, 'dt', undefined, label), + createElement(documentRef, 'dd', undefined, valueOrDash(value)) + ); + }); + parent.append(list); +}; + +const statusBadge = ( + documentRef: Document, + label: string, + value: boolean | undefined, + pendingLabel: string +) => { + const state = value === true ? 'valid' : value === false ? 'invalid' : 'pending'; + const text = value === true ? `${label}: valid` : value === false ? `${label}: invalid` : `${label}: ${pendingLabel}`; + const element = createElement(documentRef, 'span', 'signature-status', text); + element.dataset.state = state; + return element; +}; + +const renderSigner = ( + documentRef: Document, + signer: SignatureSignerSummary +) => { + const item = createElement(documentRef, 'article', 'signature-item'); + const head = createElement(documentRef, 'div', 'signature-item-head'); + head.append( + createElement(documentRef, 'strong', undefined, `Signer ${signer.index + 1}`), + createElement(documentRef, 'span', undefined, signer.certificateIndex === undefined + ? 'certificate not matched' + : `certificate ${signer.certificateIndex + 1}`) + ); + const statuses = createElement(documentRef, 'div', 'signature-status-list'); + statuses.append( + statusBadge(documentRef, 'Signature', signer.cryptographicSignatureValid, 'not verified'), + statusBadge(documentRef, 'Content digest', signer.digestMatches, 'original required') + ); + item.append(head, statuses); + appendDefinitionList(documentRef, item, [ + ['Signer identifier', signer.sid], + ['Digest algorithm', signer.digestAlgorithm], + ['Signature algorithm', signer.signatureAlgorithm], + ['Signing time', signer.signingTime], + ['Signed content type', signer.contentTypeOid], + ['Signed message digest', signer.messageDigest], + ['CAdES signingCertificateV2', signer.signingCertificateV2 ? 'Present' : 'Not detected'], + ['Signature timestamp tokens', signer.signatureTimestampTokens], + ['Verification note', signer.verificationError], + ]); + return item; +}; + +const renderCertificate = ( + documentRef: Document, + certificate: SignatureCertificateSummary +) => { + const item = createElement(documentRef, 'article', 'signature-item'); + const head = createElement(documentRef, 'div', 'signature-item-head'); + head.append( + createElement(documentRef, 'strong', undefined, `Certificate ${certificate.index + 1}`), + createElement(documentRef, 'span', undefined, `X.509 v${certificate.version || '?'}`) + ); + item.append(head); + appendDefinitionList(documentRef, item, [ + ['Subject', certificate.subject], + ['Issuer', certificate.issuer], + ['Serial number', certificate.serialNumber], + ['Valid from', certificate.notBefore], + ['Valid until', certificate.notAfter], + ['Public key', certificate.publicKeyAlgorithm], + ['Public key curve', certificate.publicKeyCurveOid], + ['Certificate signature', certificate.signatureAlgorithm], + ['Subject key identifier', certificate.subjectKeyIdentifier], + ['SHA-256 fingerprint', certificate.fingerprintSha256], + ]); + return item; +}; + +const renderTimestamp = ( + documentRef: Document, + timestamp: TimestampInfoSummary +) => { + const card = createElement(documentRef, 'section', 'signature-card'); + card.append(createElement(documentRef, 'h3', undefined, 'RFC 3161 timestamp')); + const statuses = createElement(documentRef, 'div', 'signature-status-list'); + statuses.append(statusBadge( + documentRef, + 'Message imprint', + timestamp.messageImprintMatchesOriginal, + 'original required' + )); + card.append(statuses); + appendDefinitionList(documentRef, card, [ + ['Version', timestamp.version], + ['Policy OID', timestamp.policyOid], + ['Serial number', timestamp.serialNumber], + ['Generation time', timestamp.generationTime], + ['Message imprint algorithm', timestamp.messageImprintAlgorithm], + ['Message imprint', timestamp.messageImprint], + ['Nonce', timestamp.nonce], + ['TSA certificate requested', timestamp.certReq === undefined ? undefined : timestamp.certReq ? 'Yes' : 'No'], + ['Ordering', timestamp.ordering], + ['Accuracy', timestamp.accuracy + ? [ + timestamp.accuracy.seconds === undefined ? '' : `${timestamp.accuracy.seconds}s`, + timestamp.accuracy.millis === undefined ? '' : `${timestamp.accuracy.millis}ms`, + timestamp.accuracy.micros === undefined ? '' : `${timestamp.accuracy.micros}µs`, + ].filter(Boolean).join(' ') + : undefined], + ['Timestamp authority', timestamp.tsa], + ]); + return card; +}; + +const getFilenameExtension = (filename: string) => { + const basename = filename.split(/[\\/]/).pop() || filename; + const dot = basename.lastIndexOf('.'); + return dot > 0 && dot < basename.length - 1 ? basename.slice(dot + 1).toLowerCase() : ''; +}; + +const stripContainerExtension = (filename: string) => filename.replace( + /\.(?:p7m|p7s|p7b|p7c|pkcs7|cms|cmsc|tst|tsq|tsr|tsd)$/i, + '' +); + +const sniffNestedExtension = (bytes: Uint8Array) => { + const head = bytes.subarray(0, Math.min(bytes.byteLength, 512)); + const ascii = String.fromCharCode(...head); + if (ascii.startsWith('%PDF-')) return 'pdf'; + if (head[0] === 0x89 && ascii.slice(1, 4) === 'PNG') return 'png'; + if (head[0] === 0xff && head[1] === 0xd8) return 'jpg'; + if (ascii.startsWith('GIF87a') || ascii.startsWith('GIF89a')) return 'gif'; + if (ascii.startsWith('PK\u0003\u0004')) return 'zip'; + const text = new TextDecoder('utf-8', { fatal: false }).decode(head).trimStart(); + if (text.startsWith(' ( + value === 9 || value === 10 || value === 13 || (value >= 32 && value < 127) + )).length / head.length > 0.92; + return printable ? 'txt' : 'bin'; +}; + +const resolveNestedIdentity = ( + inspection: SignatureInspection, + sourceFilename: string, + originalFilename?: string, + preferOriginal = false +) => { + if (preferOriginal && originalFilename) { + return { + filename: originalFilename, + extension: getFilenameExtension(originalFilename) || 'bin', + }; + } + const stripped = stripContainerExtension(sourceFilename); + const extension = getFilenameExtension(stripped) || ( + inspection.embeddedContent ? sniffNestedExtension(inspection.embeddedContent) : 'bin' + ); + const filename = getFilenameExtension(stripped) + ? stripped + : `signed-content.${extension}`; + return { filename, extension }; +}; + +const readConfiguredOriginal = async (options?: FileViewerSignatureOptions) => { + if (!options?.originalContent) { + return undefined; + } + if (options.originalContent instanceof ArrayBuffer) { + return options.originalContent; + } + return options.originalContent.arrayBuffer(); +}; + +const readConfiguredOpenPgpKeys = async (options?: FileViewerSignatureOptions) => Promise.all( + (options?.openPgpPublicKeys || []).map(key => key instanceof ArrayBuffer ? key : key.arrayBuffer()) +); + +const renderOpenPgpKey = (documentRef: Document, key: OpenPgpInspectionResult['keys'][number]) => { + const item = createElement(documentRef, 'article', 'signature-item'); + const head = createElement(documentRef, 'div', 'signature-item-head'); + head.append( + createElement(documentRef, 'strong', undefined, `${key.kind === 'private' ? 'Private-key block' : 'Public key'}`), + createElement(documentRef, 'span', undefined, key.keyId || 'key id unavailable') + ); + item.append(head); + appendDefinitionList(documentRef, item, [ + ['Fingerprint', key.fingerprint], + ['Version', key.version], + ['Algorithm', key.algorithm], + ['Created', key.createdAt], + ['User IDs', key.userIds.join('; ')], + ['Subkeys', key.subkeys.length], + ]); + if (key.kind === 'private') { + item.append(createElement( + documentRef, + 'div', + 'signature-warning', + 'Only public metadata is exposed. Secret MPIs, private scalars and passphrases never cross the WASM boundary.' + )); + } + return item; +}; + +export default async function renderSignature( + buffer: ArrayBuffer, + target: HTMLDivElement, + type?: string, + context?: FileRenderContext +): Promise { + const documentRef = target.ownerDocument; + const options = (context?.options as SignatureFileViewerOptions | undefined)?.signature; + const filename = context?.filename || `signature.${type || 'p7m'}`; + const maxContainerSize = options?.maxContainerSize || DEFAULT_MAX_CONTAINER_SIZE; + const maxNestedPreviewSize = options?.maxNestedPreviewSize || DEFAULT_MAX_NESTED_PREVIEW_SIZE; + if (buffer.byteLength > maxContainerSize) { + throw new Error(`The cryptographic container (${formatBytes(buffer.byteLength)}) exceeds the ${formatBytes(maxContainerSize)} parsing limit.`); + } + + const style = createElement(documentRef, 'style'); + style.textContent = signatureStyle; + const root = createElement(documentRef, 'section', 'signature-shell'); + root.dataset.viewerDensity = normalizeFileViewerUiDensity(context?.options?.ui?.density); + target.replaceChildren(style, root); + + let originalContent = await readConfiguredOriginal(options); + let originalFilename = options?.originalFilename; + let openPgpPublicKeys = await readConfiguredOpenPgpKeys(options); + let openPgpClient: OpenPgpWorkerClientType | undefined; + let nestedRendered: FileViewerRenderedInstance | undefined; + let disposed = false; + let renderSequence = 0; + + const clearNested = async () => { + await disposeFileViewerRendered(nestedRendered); + nestedRendered = undefined; + }; + + const renderInspection = async () => { + const sequence = ++renderSequence; + await clearNested(); + root.replaceChildren(createElement(documentRef, 'div', 'signature-loading')); + const loading = root.firstElementChild as HTMLElement; + loading.append( + createElement(documentRef, 'span', 'signature-spinner'), + createElement(documentRef, 'span', undefined, 'Inspecting signature and timestamp structures locally…') + ); + + try { + const sourceBytes = new Uint8Array(buffer); + if (isProbablyOpenPgp(sourceBytes, filename, type)) { + const { OpenPgpWorkerClient } = await import('./openpgp/client.js'); + openPgpClient ||= new OpenPgpWorkerClient(); + const inspection = await openPgpClient.inspect(buffer, options?.openPgpLimits); + let verification: OpenPgpVerificationResult | undefined; + if (inspection.classification === 'detached-signature' && originalContent && openPgpPublicKeys.length) { + verification = await openPgpClient.verifyDetached( + originalContent, + buffer, + openPgpPublicKeys, + options?.openPgpLimits + ); + } + if (disposed || sequence !== renderSequence) return; + + root.replaceChildren(); + const header = createElement(documentRef, 'header', 'signature-header'); + const title = createElement(documentRef, 'div', 'signature-title'); + title.append( + createElement(documentRef, 'small', undefined, 'OPENPGP · rPGP / WASM'), + createElement(documentRef, 'h2', undefined, filename), + createElement(documentRef, 'p', undefined, inspection.classification) + ); + header.append(title, createElement(documentRef, 'span', 'signature-phase', 'OPTIONAL · LAZY WORKER / WASM')); + + const content = createElement(documentRef, 'div', 'signature-content'); + const sidebar = createElement(documentRef, 'aside', 'signature-sidebar'); + const main = createElement(documentRef, 'main', 'signature-main'); + + const summary = createElement(documentRef, 'section', 'signature-card'); + summary.append(createElement(documentRef, 'h3', undefined, 'OpenPGP summary')); + appendDefinitionList(documentRef, summary, [ + ['Classification', inspection.classification], + ['ASCII armored', inspection.armored ? 'Yes' : 'No'], + ['Armor type', inspection.armorType], + ['Encrypted', inspection.encrypted ? 'Yes' : 'No'], + ['Compressed', inspection.compressed ? 'Yes' : 'No'], + ['Packet count', inspection.packetCount], + ['Packet types', inspection.packetTypes.join(', ')], + ['Keys', inspection.keys.length], + ['Signatures', inspection.signatures.length], + ]); + summary.append(createElement( + documentRef, + 'div', + 'signature-openpgp-note', + 'OpenPGP parsing runs browser-local inside a dedicated Worker backed by rPGP WebAssembly. OpenPGP.js and GnuPG are not used.' + )); + sidebar.append(summary); + + const warnings = createElement(documentRef, 'section', 'signature-card'); + warnings.append(createElement(documentRef, 'h3', undefined, 'Security boundaries')); + [ + ...inspection.warnings, + 'Parsing does not imply signature verification, key trust, identity trust, or legal validity.', + 'Private-key decryption, signing, key generation and message decryption are intentionally disabled in this phase.', + ].forEach(warning => warnings.append(createElement(documentRef, 'div', 'signature-warning', warning))); + sidebar.append(warnings); + + if (inspection.classification === 'detached-signature') { + const verifyCard = createElement(documentRef, 'section', 'signature-card'); + verifyCard.append(createElement(documentRef, 'h3', undefined, 'Detached signature verification')); + const statusList = createElement(documentRef, 'div', 'signature-status-list'); + statusList.append(statusBadge( + documentRef, + 'Cryptographic signature', + verification?.valid, + originalContent ? (openPgpPublicKeys.length ? 'not verified' : 'public key required') : 'original required' + )); + verifyCard.append(statusList); + appendDefinitionList(documentRef, verifyCard, [ + ['Result', verification?.status], + ['Key fingerprint', verification?.keyFingerprint], + ['Key ID', verification?.keyId], + ['Verification note', verification?.error], + ]); + + const originalBox = createElement(documentRef, 'div', 'signature-original'); + const originalInput = createElement(documentRef, 'input', 'signature-file') as HTMLInputElement; + originalInput.type = 'file'; + originalInput.setAttribute('aria-label', 'Choose original OpenPGP signed content'); + originalInput.addEventListener('change', () => { + const file = originalInput.files?.[0]; + if (!file) return; + void file.arrayBuffer().then(next => { + originalContent = next; + originalFilename = file.name; + return renderInspection(); + }); + }); + originalBox.append(originalInput, createElement( + documentRef, + 'div', + 'signature-original-state', + originalContent ? `${originalFilename || 'Host-provided original'} · ${formatBytes(originalContent.byteLength)}` : 'Original content is required.' + )); + + const keyBox = createElement(documentRef, 'div', 'signature-key-input'); + const keyInput = createElement(documentRef, 'input', 'signature-file') as HTMLInputElement; + keyInput.type = 'file'; + keyInput.multiple = true; + keyInput.setAttribute('aria-label', 'Choose OpenPGP public verification keys'); + keyInput.addEventListener('change', () => { + const files = Array.from(keyInput.files || []); + void Promise.all(files.map(file => file.arrayBuffer())).then(keys => { + openPgpPublicKeys = keys; + return renderInspection(); + }); + }); + keyBox.append(keyInput, createElement( + documentRef, + 'div', + 'signature-original-state', + openPgpPublicKeys.length ? `${openPgpPublicKeys.length} verification key file(s) supplied.` : 'Supply one or more public keys; no keyring lookup is performed.' + )); + verifyCard.append(originalBox, keyBox); + sidebar.append(verifyCard); + } + + if (inspection.keys.length) { + const keysCard = createElement(documentRef, 'section', 'signature-card'); + keysCard.append(createElement(documentRef, 'h3', undefined, 'Key metadata')); + inspection.keys.forEach(key => keysCard.append(renderOpenPgpKey(documentRef, key))); + main.append(keysCard); + } + + const previewCard = createElement(documentRef, 'section', 'signature-card signature-preview'); + const previewHead = createElement(documentRef, 'div', 'signature-preview-head'); + const previewTarget = createElement(documentRef, 'div', 'signature-preview-target') as HTMLDivElement; + const literal = inspection.literalData; + const literalBytes = literal?.data; + const literalFilename = literal?.filename || 'openpgp-literal-data.bin'; + const literalExtension = getFilenameExtension(literalFilename) || (literalBytes ? sniffNestedExtension(literalBytes) : 'bin'); + previewHead.append( + createElement(documentRef, 'strong', undefined, `Literal-data preview · ${literalFilename}`), + createElement(documentRef, 'span', 'signature-status', literalBytes ? formatBytes(literalBytes.byteLength) : 'not extracted') + ); + previewCard.append(previewHead, previewTarget); + main.append(previewCard); + if (!literalBytes) { + previewTarget.append(createElement( + documentRef, + 'div', + 'signature-preview-empty', + inspection.encrypted + ? 'Encrypted OpenPGP data is inspection-only in this phase.' + : inspection.compressed + ? 'Compressed OpenPGP data is not recursively decompressed unless a bounded path is implemented.' + : 'No safely extractable literal data is available.' + )); + } else if (literalBytes.byteLength > maxNestedPreviewSize) { + previewTarget.append(createElement(documentRef, 'div', 'signature-preview-empty', 'Extracted literal data exceeds the nested-preview limit.')); + } else if (!context?.renderNestedBuffer || literalExtension === 'bin') { + previewTarget.append(createElement(documentRef, 'div', 'signature-preview-empty', 'Literal data was extracted, but no compatible nested renderer is available.')); + } else { + nestedRendered = await context.renderNestedBuffer( + literalBytes.buffer.slice(literalBytes.byteOffset, literalBytes.byteOffset + literalBytes.byteLength) as ArrayBuffer, + literalExtension, + previewTarget, + { ...context, filename: literalFilename, url: undefined, streamUrl: undefined } + ); + } + + content.append(sidebar, main); + root.append(header, content); + return; + } + + const inspection = await inspectSignatureContainer(buffer, { + sourceFilename: filename, + extensionHint: type, + originalContent, + }); + if (disposed || sequence !== renderSequence) { + return; + } + root.replaceChildren(); + const header = createElement(documentRef, 'header', 'signature-header'); + const title = createElement(documentRef, 'div', 'signature-title'); + title.append( + createElement(documentRef, 'small', undefined, 'CRYPTOGRAPHIC CONTAINER'), + createElement(documentRef, 'h2', undefined, filename), + createElement(documentRef, 'p', undefined, inspection.detectedFormat) + ); + header.append(title, createElement(documentRef, 'span', 'signature-phase', 'PHASE 1 · CMS / RFC 3161 / RFC 5544')); + + const content = createElement(documentRef, 'div', 'signature-content'); + const sidebar = createElement(documentRef, 'aside', 'signature-sidebar'); + const main = createElement(documentRef, 'main', 'signature-main'); + + const summary = createElement(documentRef, 'section', 'signature-card'); + summary.append(createElement(documentRef, 'h3', undefined, 'Container summary')); + appendDefinitionList(documentRef, summary, [ + ['Detected format', inspection.detectedFormat], + ['Size', formatBytes(inspection.sourceSize)], + ['CMS content type', inspection.contentType], + ['Signed content type', inspection.signedContentType], + ['Digest algorithms', inspection.digestAlgorithms.join(', ')], + ['Encapsulated content', inspection.embeddedContent ? formatBytes(inspection.embeddedContent.byteLength) : 'No'], + ['Detached', inspection.detached ? 'Yes' : 'No'], + ['Signers', inspection.signers.length], + ['Certificates', inspection.certificates.length], + ['CRLs', inspection.crlCount], + ]); + sidebar.append(summary); + + const boundaries = createElement(documentRef, 'section', 'signature-card'); + boundaries.append(createElement(documentRef, 'h3', undefined, 'Validation boundaries')); + inspection.warnings.forEach(warning => boundaries.append( + createElement(documentRef, 'div', 'signature-warning', warning) + )); + sidebar.append(boundaries); + + if (inspection.timestampResponse) { + const response = createElement(documentRef, 'section', 'signature-card'); + response.append(createElement(documentRef, 'h3', undefined, 'Timestamp response')); + appendDefinitionList(documentRef, response, [ + ['Status', inspection.timestampResponse.statusLabel], + ['Status code', inspection.timestampResponse.status], + ['Status text', inspection.timestampResponse.statusText.join('; ')], + ['Failure info', inspection.timestampResponse.failureInfo], + ]); + sidebar.append(response); + } + + if (inspection.timestampedData) { + const tsd = createElement(documentRef, 'section', 'signature-card'); + tsd.append(createElement(documentRef, 'h3', undefined, 'RFC 5544 TimeStampedData')); + appendDefinitionList(documentRef, tsd, [ + ['Data URI', inspection.timestampedData.dataUri], + ['Filename', inspection.timestampedData.filename], + ['Media type', inspection.timestampedData.mediaType], + ['Metadata hash protected', inspection.timestampedData.hashProtected === undefined ? undefined : inspection.timestampedData.hashProtected ? 'Yes' : 'No'], + ['Temporal evidence entries', inspection.timestampedData.temporalEvidenceEntries], + ]); + sidebar.append(tsd); + } + + if (inspection.requiresOriginalContent || inspection.timestamp?.messageImprint || inspection.kind === 'timestamped-data') { + const originalCard = createElement(documentRef, 'section', 'signature-card'); + originalCard.append(createElement(documentRef, 'h3', undefined, 'Original content')); + const originalBox = createElement(documentRef, 'div', 'signature-original'); + const input = createElement(documentRef, 'input', 'signature-file') as HTMLInputElement; + input.type = 'file'; + input.setAttribute('aria-label', 'Choose original content'); + const originalState = createElement( + documentRef, + 'div', + 'signature-original-state', + originalContent + ? `${originalFilename || 'Host-provided content'} · ${formatBytes(originalContent.byteLength)}` + : 'Choose the original file to compare its digest locally.' + ); + input.addEventListener('change', () => { + const file = input.files?.[0]; + if (!file) return; + void file.arrayBuffer().then(nextBuffer => { + originalContent = nextBuffer; + originalFilename = file.name; + return renderInspection(); + }).catch(error => { + originalState.textContent = error instanceof Error ? error.message : String(error); + }); + }); + originalBox.append(input, originalState); + originalCard.append(originalBox); + sidebar.append(originalCard); + } + + if (inspection.signers.length) { + const signers = createElement(documentRef, 'section', 'signature-card'); + signers.append(createElement(documentRef, 'h3', undefined, 'Signers')); + inspection.signers.forEach(signer => signers.append(renderSigner(documentRef, signer))); + main.append(signers); + } + + if (inspection.timestamp) { + main.append(renderTimestamp(documentRef, inspection.timestamp)); + } + + if (inspection.certificates.length) { + const certificates = createElement(documentRef, 'section', 'signature-card'); + certificates.append(createElement(documentRef, 'h3', undefined, 'Included certificates')); + inspection.certificates.forEach(certificate => certificates.append(renderCertificate(documentRef, certificate))); + main.append(certificates); + } + + const previewCard = createElement(documentRef, 'section', 'signature-card signature-preview'); + const previewHead = createElement(documentRef, 'div', 'signature-preview-head'); + const previewTarget = createElement(documentRef, 'div', 'signature-preview-target') as HTMLDivElement; + const timestampContainer = inspection.kind === 'timestamp-request' || + inspection.kind === 'timestamp-response' || + inspection.kind === 'timestamp-token' || + inspection.kind === 'timestamped-data'; + const previewUsesOriginal = Boolean(originalContent && (inspection.detached || timestampContainer)); + const previewSource = previewUsesOriginal ? originalContent : inspection.embeddedContent; + const previewBytes = previewSource + ? previewSource instanceof Uint8Array + ? previewSource + : new Uint8Array(previewSource) + : undefined; + const identity = inspection.kind === 'timestamped-data' && inspection.timestampedData?.filename && !previewUsesOriginal + ? { + filename: inspection.timestampedData.filename, + extension: getFilenameExtension(inspection.timestampedData.filename) || sniffNestedExtension(previewBytes || new Uint8Array()), + } + : resolveNestedIdentity(inspection, filename, originalFilename, previewUsesOriginal); + previewHead.append( + createElement(documentRef, 'strong', undefined, `Document preview · ${identity.filename}`), + createElement(documentRef, 'span', 'signature-status', previewBytes ? formatBytes(previewBytes.byteLength) : 'content unavailable') + ); + previewCard.append(previewHead, previewTarget); + main.append(previewCard); + + content.append(sidebar, main); + root.append(header, content); + + if (!previewBytes) { + previewTarget.append(createElement( + documentRef, + 'div', + 'signature-preview-empty', + inspection.requiresOriginalContent + ? 'Supply the original content to compare its digest or timestamp message imprint and open it through the nested renderer.' + : 'The container does not include previewable document content.' + )); + } else if (previewBytes.byteLength > maxNestedPreviewSize) { + previewTarget.append(createElement( + documentRef, + 'div', + 'signature-preview-empty', + `Nested preview is disabled because ${formatBytes(previewBytes.byteLength)} exceeds the ${formatBytes(maxNestedPreviewSize)} limit.` + )); + } else if (!context?.renderNestedBuffer || identity.extension === 'bin') { + previewTarget.append(createElement( + documentRef, + 'div', + 'signature-preview-empty', + 'The content was extracted and verified, but no compatible nested renderer is available for this file type.' + )); + } else { + nestedRendered = await context.renderNestedBuffer( + previewBytes.buffer.slice( + previewBytes.byteOffset, + previewBytes.byteOffset + previewBytes.byteLength + ) as ArrayBuffer, + identity.extension, + previewTarget, + { + ...context, + filename: identity.filename, + url: undefined, + streamUrl: undefined, + } + ); + } + } catch (error) { + if (disposed || sequence !== renderSequence) { + return; + } + root.replaceChildren(createElement( + documentRef, + 'div', + 'signature-error', + error instanceof Error ? error.message : String(error) + )); + } + }; + + await renderInspection(); + + return { + $el: root, + async unmount() { + disposed = true; + renderSequence += 1; + openPgpClient?.dispose(); + openPgpClient = undefined; + await clearNested(); + target.replaceChildren(); + }, + }; +} diff --git a/packages/renderers/signature/src/signature.worker.ts b/packages/renderers/signature/src/signature.worker.ts new file mode 100644 index 000000000..9dec37b6a --- /dev/null +++ b/packages/renderers/signature/src/signature.worker.ts @@ -0,0 +1,87 @@ +/// +import type { SignatureWorkerRequest, SignatureWorkerResponse } from './workerProtocol.js'; + +type RpgpWasmModule = { + default(input?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module): Promise; + classify_openpgp(input: Uint8Array, limits: unknown): unknown; + inspect_openpgp(input: Uint8Array, limits: unknown): unknown; + verify_detached_signature(content: Uint8Array, signature: Uint8Array, publicKeys: Uint8Array[], limits: unknown): unknown; +}; + +const scope = self as unknown as DedicatedWorkerGlobalScope; +let wasmPromise: Promise | undefined; + +const loadWasm = async () => { + if (wasmPromise) return wasmPromise; + wasmPromise = (async () => { + try { + const moduleUrl = new URL('./rpgp-wasm/rpgp_wrapper.js', import.meta.url).href; + const wasmUrl = new URL('./rpgp-wasm/rpgp_wrapper_bg.wasm', import.meta.url); + const module = await import(/* @vite-ignore */ moduleUrl) as RpgpWasmModule; + await module.default(wasmUrl); + return module; + } catch (error) { + wasmPromise = undefined; + throw Object.assign( + new Error(error instanceof Error ? error.message : String(error)), + { code: 'wasm-initialization-failed' } + ); + } + })(); + return wasmPromise; +}; + +const errorResponse = (id: string, error: unknown): SignatureWorkerResponse => ({ + id, + ok: false, + error: { + code: typeof error === 'object' && error && 'code' in error + ? String((error as { code?: unknown }).code) as never + : 'internal-parser-error', + message: error instanceof Error ? error.message : String(error), + }, +}); + +scope.addEventListener('message', async (event: MessageEvent) => { + const request = event.data; + try { + const wasm = await loadWasm(); + let response: SignatureWorkerResponse; + switch (request.type) { + case 'classify': + response = { + id: request.id, + ok: true, + type: 'classify', + result: wasm.classify_openpgp(new Uint8Array(request.input), request.limits) as never, + }; + break; + case 'inspect': + response = { + id: request.id, + ok: true, + type: 'inspect', + result: wasm.inspect_openpgp(new Uint8Array(request.input), request.limits) as never, + }; + break; + case 'verify-detached': + response = { + id: request.id, + ok: true, + type: 'verify-detached', + result: wasm.verify_detached_signature( + new Uint8Array(request.content), + new Uint8Array(request.signature), + request.publicKeys.map(value => new Uint8Array(value)), + request.limits + ) as never, + }; + break; + default: + throw Object.assign(new Error('Unknown signature Worker request.'), { code: 'invalid-input' }); + } + scope.postMessage(response); + } catch (error) { + scope.postMessage(errorResponse(request.id, error)); + } +}); diff --git a/packages/renderers/signature/src/signatureAsn1.ts b/packages/renderers/signature/src/signatureAsn1.ts new file mode 100644 index 000000000..06921230a --- /dev/null +++ b/packages/renderers/signature/src/signatureAsn1.ts @@ -0,0 +1,1505 @@ +const UNIVERSAL = 0; +const CONTEXT = 2; + +const TAG = { + boolean: 1, + integer: 2, + bitString: 3, + octetString: 4, + null: 5, + oid: 6, + utf8String: 12, + sequence: 16, + set: 17, + printableString: 19, + teletexString: 20, + ia5String: 22, + utcTime: 23, + generalizedTime: 24, + universalString: 28, + bmpString: 30, +} as const; + +const OID = { + data: '1.2.840.113549.1.7.1', + signedData: '1.2.840.113549.1.7.2', + envelopedData: '1.2.840.113549.1.7.3', + digestedData: '1.2.840.113549.1.7.5', + encryptedData: '1.2.840.113549.1.7.6', + tstInfo: '1.2.840.113549.1.9.16.1.4', + timestampedData: '1.2.840.113549.1.9.16.1.31', + contentTypeAttribute: '1.2.840.113549.1.9.3', + messageDigestAttribute: '1.2.840.113549.1.9.4', + signingTimeAttribute: '1.2.840.113549.1.9.5', + signatureTimestampTokenAttribute: '1.2.840.113549.1.9.16.2.14', + signingCertificateV2Attribute: '1.2.840.113549.1.9.16.2.47', + subjectKeyIdentifier: '2.5.29.14', + sha1: '1.3.14.3.2.26', + sha256: '2.16.840.1.101.3.4.2.1', + sha384: '2.16.840.1.101.3.4.2.2', + sha512: '2.16.840.1.101.3.4.2.3', + rsaEncryption: '1.2.840.113549.1.1.1', + sha1WithRsa: '1.2.840.113549.1.1.5', + sha256WithRsa: '1.2.840.113549.1.1.11', + sha384WithRsa: '1.2.840.113549.1.1.12', + sha512WithRsa: '1.2.840.113549.1.1.13', + rsaPss: '1.2.840.113549.1.1.10', + ecPublicKey: '1.2.840.10045.2.1', + ecdsaSha256: '1.2.840.10045.4.3.2', + ecdsaSha384: '1.2.840.10045.4.3.3', + ecdsaSha512: '1.2.840.10045.4.3.4', + ed25519: '1.3.101.112', + prime256v1: '1.2.840.10045.3.1.7', + secp384r1: '1.3.132.0.34', + secp521r1: '1.3.132.0.35', +} as const; + +const OID_LABELS: Readonly> = { + [OID.data]: 'CMS data', + [OID.signedData]: 'CMS SignedData', + [OID.envelopedData]: 'CMS EnvelopedData', + [OID.digestedData]: 'CMS DigestedData', + [OID.encryptedData]: 'CMS EncryptedData', + [OID.tstInfo]: 'RFC 3161 TSTInfo', + [OID.timestampedData]: 'RFC 5544 TimeStampedData', + [OID.sha1]: 'SHA-1', + [OID.sha256]: 'SHA-256', + [OID.sha384]: 'SHA-384', + [OID.sha512]: 'SHA-512', + [OID.rsaEncryption]: 'RSA', + [OID.sha1WithRsa]: 'SHA-1 with RSA', + [OID.sha256WithRsa]: 'SHA-256 with RSA', + [OID.sha384WithRsa]: 'SHA-384 with RSA', + [OID.sha512WithRsa]: 'SHA-512 with RSA', + [OID.rsaPss]: 'RSA-PSS', + [OID.ecPublicKey]: 'EC public key', + [OID.ecdsaSha256]: 'ECDSA with SHA-256', + [OID.ecdsaSha384]: 'ECDSA with SHA-384', + [OID.ecdsaSha512]: 'ECDSA with SHA-512', + [OID.ed25519]: 'Ed25519', +}; + +const NAME_LABELS: Readonly> = { + '2.5.4.3': 'CN', + '2.5.4.4': 'SN', + '2.5.4.5': 'serialNumber', + '2.5.4.6': 'C', + '2.5.4.7': 'L', + '2.5.4.8': 'ST', + '2.5.4.10': 'O', + '2.5.4.11': 'OU', + '2.5.4.12': 'title', + '2.5.4.42': 'GN', + '1.2.840.113549.1.9.1': 'emailAddress', +}; + +export type SignatureContainerKind = + | 'cms-signed-data' + | 'cms-certificates' + | 'cms-content' + | 'timestamp-request' + | 'timestamp-response' + | 'timestamp-token' + | 'timestamped-data' + | 'unknown'; + +export interface SignatureCertificateSummary { + index: number; + version?: number; + serialNumber: string; + subject: string; + issuer: string; + notBefore?: string; + notAfter?: string; + signatureAlgorithmOid?: string; + signatureAlgorithm?: string; + publicKeyAlgorithmOid?: string; + publicKeyAlgorithm?: string; + publicKeyCurveOid?: string; + subjectKeyIdentifier?: string; + fingerprintSha256?: string; +} + +export interface SignatureSignerSummary { + index: number; + sid: string; + digestAlgorithmOid?: string; + digestAlgorithm?: string; + signatureAlgorithmOid?: string; + signatureAlgorithm?: string; + signingTime?: string; + messageDigest?: string; + contentTypeOid?: string; + certificateIndex?: number; + digestMatches?: boolean; + cryptographicSignatureValid?: boolean; + verificationError?: string; + signatureTimestampTokens: number; + signingCertificateV2: boolean; +} + +export interface TimestampAccuracySummary { + seconds?: number; + millis?: number; + micros?: number; +} + +export interface TimestampInfoSummary { + version?: number; + policyOid?: string; + serialNumber?: string; + generationTime?: string; + accuracy?: TimestampAccuracySummary; + ordering?: boolean; + nonce?: string; + tsa?: string; + messageImprintAlgorithmOid?: string; + messageImprintAlgorithm?: string; + messageImprint?: string; + messageImprintMatchesOriginal?: boolean; + certReq?: boolean; +} + +export interface TimestampResponseSummary { + status?: number; + statusLabel?: string; + statusText: string[]; + failureInfo?: string; +} + +export interface SignatureInspection { + kind: SignatureContainerKind; + detectedFormat: string; + sourceSize: number; + contentTypeOid?: string; + contentType?: string; + signedContentTypeOid?: string; + signedContentType?: string; + digestAlgorithms: string[]; + embeddedContent?: Uint8Array; + detached: boolean; + requiresOriginalContent: boolean; + originalContentSupplied: boolean; + certificates: SignatureCertificateSummary[]; + signers: SignatureSignerSummary[]; + timestamp?: TimestampInfoSummary; + timestampResponse?: TimestampResponseSummary; + crlCount: number; + timestampedData?: { + dataUri?: string; + filename?: string; + mediaType?: string; + hashProtected?: boolean; + temporalEvidenceEntries: number; + }; + warnings: string[]; +} + +export interface InspectSignatureOptions { + sourceFilename?: string; + extensionHint?: string; + originalContent?: ArrayBuffer | Uint8Array; +} + +interface Asn1Node { + tagClass: number; + tagNumber: number; + constructed: boolean; + start: number; + headerEnd: number; + contentStart: number; + contentEnd: number; + end: number; + indefinite: boolean; + children: Asn1Node[]; +} + +interface ParseState { + bytes: Uint8Array; + nodes: number; + maxNodes: number; + maxDepth: number; +} + +interface CertificateRecord { + summary: SignatureCertificateSummary; + node: Asn1Node; + issuerDer: string; + serialDer: string; + subjectKeyIdentifier?: string; + spki: Uint8Array; + publicKeyAlgorithmOid?: string; + publicKeyCurveName?: 'P-256' | 'P-384' | 'P-521'; +} + +interface SignerRecord { + summary: SignatureSignerSummary; + issuerDer?: string; + serialDer?: string; + subjectKeyIdentifier?: string; + digestAlgorithmNode?: Asn1Node; + signatureAlgorithmNode?: Asn1Node; + signedAttributesNode?: Asn1Node; + signature?: Uint8Array; +} + +interface CmsRecord { + contentTypeOid: string; + signedContentTypeOid?: string; + digestAlgorithmOids: string[]; + embeddedContent?: Uint8Array; + certificates: CertificateRecord[]; + crlCount: number; + signers: SignerRecord[]; +} + +const toUint8Array = (value: ArrayBuffer | Uint8Array) => value instanceof Uint8Array + ? value + : new Uint8Array(value); + +const bytesToHex = (bytes: Uint8Array, separator = '') => Array.from( + bytes, + value => value.toString(16).padStart(2, '0') +).join(separator).toUpperCase(); + +const equalBytes = (left: Uint8Array, right: Uint8Array) => { + if (left.byteLength !== right.byteLength) { + return false; + } + let mismatch = 0; + for (let index = 0; index < left.byteLength; index += 1) { + mismatch |= left[index]! ^ right[index]!; + } + return mismatch === 0; +}; + +const formatOid = (oid?: string) => oid + ? OID_LABELS[oid] || oid + : 'Unknown'; + +const assertBounds: (condition: unknown, message: string) => asserts condition = (condition, message) => { + if (!condition) { + throw new Error(message); + } +}; + +const parseNode = ( + state: ParseState, + offset: number, + limit: number, + depth: number +): { node: Asn1Node; next: number } => { + assertBounds(depth <= state.maxDepth, `ASN.1 nesting exceeds ${state.maxDepth}.`); + assertBounds(offset < limit, 'Unexpected end of ASN.1 input.'); + state.nodes += 1; + assertBounds(state.nodes <= state.maxNodes, `ASN.1 node count exceeds ${state.maxNodes}.`); + + const bytes = state.bytes; + const start = offset; + const identifier = bytes[offset++]!; + const tagClass = identifier >> 6; + const constructed = (identifier & 0x20) !== 0; + let tagNumber = identifier & 0x1f; + + if (tagNumber === 0x1f) { + tagNumber = 0; + let octets = 0; + for (;;) { + assertBounds(offset < limit, 'Truncated ASN.1 high-tag identifier.'); + const octet = bytes[offset++]!; + octets += 1; + assertBounds(octets <= 6, 'ASN.1 high-tag identifier is too large.'); + tagNumber = (tagNumber * 128) + (octet & 0x7f); + if ((octet & 0x80) === 0) { + break; + } + } + } + + assertBounds(offset < limit, 'Missing ASN.1 length.'); + const lengthOctet = bytes[offset++]!; + let length = 0; + let indefinite = false; + + if (lengthOctet === 0x80) { + indefinite = true; + assertBounds(constructed, 'Primitive ASN.1 values cannot use indefinite length.'); + } else if ((lengthOctet & 0x80) === 0) { + length = lengthOctet; + } else { + const lengthOctets = lengthOctet & 0x7f; + assertBounds(lengthOctets > 0 && lengthOctets <= 6, 'Unsupported ASN.1 length width.'); + assertBounds(offset + lengthOctets <= limit, 'Truncated ASN.1 length.'); + for (let index = 0; index < lengthOctets; index += 1) { + length = (length * 256) + bytes[offset++]!; + } + } + + const contentStart = offset; + const children: Asn1Node[] = []; + let contentEnd: number; + let end: number; + + if (indefinite) { + for (;;) { + assertBounds(offset + 1 < limit, 'Unterminated indefinite-length ASN.1 value.'); + if (bytes[offset] === 0 && bytes[offset + 1] === 0) { + contentEnd = offset; + end = offset + 2; + break; + } + const parsed = parseNode(state, offset, limit, depth + 1); + children.push(parsed.node); + offset = parsed.next; + } + } else { + contentEnd = contentStart + length; + assertBounds(contentEnd <= limit, 'ASN.1 length extends beyond its parent.'); + end = contentEnd; + if (constructed) { + while (offset < contentEnd) { + const parsed = parseNode(state, offset, contentEnd, depth + 1); + children.push(parsed.node); + offset = parsed.next; + } + assertBounds(offset === contentEnd, 'ASN.1 child does not end at its parent boundary.'); + } + } + + return { + node: { + tagClass, + tagNumber, + constructed, + start, + headerEnd: contentStart, + contentStart, + contentEnd: contentEnd!, + end: end!, + indefinite, + children, + }, + next: end!, + }; +}; + +const parseRoot = (bytes: Uint8Array) => { + const state: ParseState = { + bytes, + nodes: 0, + maxNodes: 25000, + maxDepth: 96, + }; + const { node, next } = parseNode(state, 0, bytes.byteLength, 0); + for (let offset = next; offset < bytes.byteLength; offset += 1) { + assertBounds(bytes[offset] === 0, 'Unexpected bytes after the ASN.1 root value.'); + } + return node; +}; + +const isTag = (node: Asn1Node | undefined, tagClass: number, tagNumber: number) => Boolean( + node && node.tagClass === tagClass && node.tagNumber === tagNumber +); + +const contentBytes = (bytes: Uint8Array, node: Asn1Node) => bytes.subarray( + node.contentStart, + node.contentEnd +); + +const fullBytes = (bytes: Uint8Array, node: Asn1Node) => bytes.subarray(node.start, node.end); + +const decodeOid = (bytes: Uint8Array, node: Asn1Node) => { + assertBounds(isTag(node, UNIVERSAL, TAG.oid), 'Expected ASN.1 OBJECT IDENTIFIER.'); + const value = contentBytes(bytes, node); + assertBounds(value.byteLength > 0, 'Empty ASN.1 OBJECT IDENTIFIER.'); + const subIdentifiers: number[] = []; + let current = 0; + let continued = false; + for (const octet of value) { + current = (current * 128) + (octet & 0x7f); + assertBounds(Number.isSafeInteger(current), 'ASN.1 OBJECT IDENTIFIER arc is too large.'); + continued = (octet & 0x80) !== 0; + if (!continued) { + subIdentifiers.push(current); + current = 0; + } + } + assertBounds(!continued, 'Truncated ASN.1 OBJECT IDENTIFIER.'); + const first = subIdentifiers.shift() || 0; + const firstArc = first < 40 ? 0 : first < 80 ? 1 : 2; + const secondArc = first - (firstArc * 40); + return [firstArc, secondArc, ...subIdentifiers].map(String).join('.'); +}; + +const integerBytes = (bytes: Uint8Array, node: Asn1Node) => { + assertBounds(isTag(node, UNIVERSAL, TAG.integer), 'Expected ASN.1 INTEGER.'); + return contentBytes(bytes, node); +}; + +const positiveIntegerBytes = (bytes: Uint8Array, node: Asn1Node) => { + let value = integerBytes(bytes, node); + while (value.byteLength > 1 && value[0] === 0) { + value = value.subarray(1); + } + return value; +}; + +const integerHex = (bytes: Uint8Array, node: Asn1Node) => bytesToHex(positiveIntegerBytes(bytes, node)); + +const integerNumber = (bytes: Uint8Array, node: Asn1Node) => { + const value = positiveIntegerBytes(bytes, node); + let result = 0; + for (const octet of value) { + result = (result * 256) + octet; + if (!Number.isSafeInteger(result)) { + return undefined; + } + } + return result; +}; + +const decodeBoolean = (bytes: Uint8Array, node: Asn1Node) => { + assertBounds(isTag(node, UNIVERSAL, TAG.boolean), 'Expected ASN.1 BOOLEAN.'); + const value = contentBytes(bytes, node); + return value.some(octet => octet !== 0); +}; + +const decodeAscii = (value: Uint8Array) => String.fromCharCode(...value); + +const decodeBmpString = (value: Uint8Array) => { + let result = ''; + for (let index = 0; index + 1 < value.byteLength; index += 2) { + result += String.fromCharCode((value[index]! << 8) | value[index + 1]!); + } + return result; +}; + +const decodeUniversalString = (value: Uint8Array) => { + let result = ''; + for (let index = 0; index + 3 < value.byteLength; index += 4) { + const codePoint = ( + (value[index]! * 0x1000000) + + (value[index + 1]! << 16) + + (value[index + 2]! << 8) + + value[index + 3]! + ); + result += String.fromCodePoint(codePoint); + } + return result; +}; + +const decodeString = (bytes: Uint8Array, node: Asn1Node) => { + const value = contentBytes(bytes, node); + switch (node.tagNumber) { + case TAG.utf8String: + return new TextDecoder('utf-8', { fatal: false }).decode(value); + case TAG.bmpString: + return decodeBmpString(value); + case TAG.universalString: + return decodeUniversalString(value); + case TAG.printableString: + case TAG.teletexString: + case TAG.ia5String: + return decodeAscii(value); + default: + return decodeAscii(value); + } +}; + +const parseTimeString = (value: string, generalized: boolean) => { + const match = generalized + ? /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(?:\.(\d+))?(Z|[+-]\d{4})$/.exec(value) + : /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(Z|[+-]\d{4})$/.exec(value); + if (!match) { + return value; + } + const year = generalized + ? Number(match[1]) + : (Number(match[1]) >= 50 ? 1900 : 2000) + Number(match[1]); + const baseIndex = generalized ? 2 : 2; + const month = Number(match[baseIndex]); + const day = Number(match[baseIndex + 1]); + const hour = Number(match[baseIndex + 2]); + const minute = Number(match[baseIndex + 3]); + const second = Number(match[baseIndex + 4]); + const fraction = generalized ? match[7] : undefined; + const zone = generalized ? match[8]! : match[7]!; + let millis = fraction ? Number(`0.${fraction}`) * 1000 : 0; + let timestamp = Date.UTC(year, month - 1, day, hour, minute, second, millis); + if (zone !== 'Z') { + const sign = zone[0] === '+' ? 1 : -1; + const offsetMinutes = (Number(zone.slice(1, 3)) * 60) + Number(zone.slice(3, 5)); + timestamp -= sign * offsetMinutes * 60_000; + } + const date = new Date(timestamp); + return Number.isNaN(date.getTime()) ? value : date.toISOString(); +}; + +const decodeTime = (bytes: Uint8Array, node: Asn1Node) => { + const raw = decodeAscii(contentBytes(bytes, node)); + if (isTag(node, UNIVERSAL, TAG.utcTime)) { + return parseTimeString(raw, false); + } + if (isTag(node, UNIVERSAL, TAG.generalizedTime)) { + return parseTimeString(raw, true); + } + return raw; +}; + +const decodeName = (bytes: Uint8Array, node: Asn1Node) => { + const rdns: string[] = []; + for (const set of node.children) { + for (const attribute of set.children) { + const [oidNode, valueNode] = attribute.children; + if (!oidNode || !valueNode || !isTag(oidNode, UNIVERSAL, TAG.oid)) { + continue; + } + const oid = decodeOid(bytes, oidNode); + const label = NAME_LABELS[oid] || oid; + rdns.push(`${label}=${decodeString(bytes, valueNode)}`); + } + } + return rdns.join(', '); +}; + +const algorithmOid = (bytes: Uint8Array, node: Asn1Node | undefined) => { + const oidNode = node?.children.find(child => isTag(child, UNIVERSAL, TAG.oid)); + return oidNode ? decodeOid(bytes, oidNode) : undefined; +}; + +const curveNameForOid = (oid?: string): CertificateRecord['publicKeyCurveName'] => { + if (oid === OID.prime256v1) return 'P-256'; + if (oid === OID.secp384r1) return 'P-384'; + if (oid === OID.secp521r1) return 'P-521'; + return undefined; +}; + +const collectOctetString = (bytes: Uint8Array, node: Asn1Node): Uint8Array => { + if (isTag(node, UNIVERSAL, TAG.octetString) && !node.constructed) { + return contentBytes(bytes, node); + } + const parts = node.children + .filter(child => isTag(child, UNIVERSAL, TAG.octetString) || child.tagClass === CONTEXT) + .map(child => collectOctetString(bytes, child)); + if (!parts.length) { + return new Uint8Array(); + } + const size = parts.reduce((sum, part) => sum + part.byteLength, 0); + const output = new Uint8Array(size); + let offset = 0; + for (const part of parts) { + output.set(part, offset); + offset += part.byteLength; + } + return output; +}; + +const parseExtensions = ( + bytes: Uint8Array, + tbsChildren: Asn1Node[] +) => { + const extensionsContainer = tbsChildren.find(child => isTag(child, CONTEXT, 3)); + const extensions = extensionsContainer?.children[0]?.children || []; + let subjectKeyIdentifier: string | undefined; + for (const extension of extensions) { + const oidNode = extension.children.find(child => isTag(child, UNIVERSAL, TAG.oid)); + const valueNode = [...extension.children].reverse().find(child => isTag(child, UNIVERSAL, TAG.octetString)); + if (!oidNode || !valueNode) { + continue; + } + const oid = decodeOid(bytes, oidNode); + if (oid === OID.subjectKeyIdentifier) { + try { + const innerBytes = contentBytes(bytes, valueNode); + const innerNode = parseRoot(innerBytes); + if (isTag(innerNode, UNIVERSAL, TAG.octetString)) { + subjectKeyIdentifier = bytesToHex(contentBytes(innerBytes, innerNode)); + } + } catch { + subjectKeyIdentifier = bytesToHex(contentBytes(bytes, valueNode)); + } + } + } + return { subjectKeyIdentifier }; +}; + +const parseCertificate = ( + bytes: Uint8Array, + node: Asn1Node, + index: number +): CertificateRecord | undefined => { + if (!isTag(node, UNIVERSAL, TAG.sequence) || node.children.length < 3) { + return undefined; + } + const [tbs, outerSignatureAlgorithm] = node.children; + if (!tbs || !isTag(tbs, UNIVERSAL, TAG.sequence)) { + return undefined; + } + const children = tbs.children; + let cursor = 0; + let version = 1; + if (isTag(children[cursor], CONTEXT, 0)) { + const versionNode = children[cursor]?.children[0]; + if (versionNode && isTag(versionNode, UNIVERSAL, TAG.integer)) { + version = (integerNumber(bytes, versionNode) || 0) + 1; + } + cursor += 1; + } + const serialNode = children[cursor++]; + const tbsSignatureAlgorithm = children[cursor++]; + const issuerNode = children[cursor++]; + const validityNode = children[cursor++]; + const subjectNode = children[cursor++]; + const spkiNode = children[cursor++]; + if (!serialNode || !issuerNode || !validityNode || !subjectNode || !spkiNode) { + return undefined; + } + const publicKeyAlgorithmNode = spkiNode.children[0]; + const publicKeyAlgorithmOid = algorithmOid(bytes, publicKeyAlgorithmNode); + const curveOidNode = publicKeyAlgorithmNode?.children[1]; + const publicKeyCurveOid = curveOidNode && isTag(curveOidNode, UNIVERSAL, TAG.oid) + ? decodeOid(bytes, curveOidNode) + : undefined; + const signatureAlgorithmOid = algorithmOid(bytes, outerSignatureAlgorithm) || algorithmOid(bytes, tbsSignatureAlgorithm); + const { subjectKeyIdentifier } = parseExtensions(bytes, children); + const issuerDer = bytesToHex(fullBytes(bytes, issuerNode)); + const serialDer = bytesToHex(positiveIntegerBytes(bytes, serialNode)); + const validity = validityNode.children.filter(child => ( + isTag(child, UNIVERSAL, TAG.utcTime) || isTag(child, UNIVERSAL, TAG.generalizedTime) + )); + + return { + node, + summary: { + index, + version, + serialNumber: serialDer, + subject: decodeName(bytes, subjectNode), + issuer: decodeName(bytes, issuerNode), + notBefore: validity[0] ? decodeTime(bytes, validity[0]) : undefined, + notAfter: validity[1] ? decodeTime(bytes, validity[1]) : undefined, + signatureAlgorithmOid, + signatureAlgorithm: formatOid(signatureAlgorithmOid), + publicKeyAlgorithmOid, + publicKeyAlgorithm: formatOid(publicKeyAlgorithmOid), + publicKeyCurveOid, + subjectKeyIdentifier, + }, + issuerDer, + serialDer, + subjectKeyIdentifier, + spki: fullBytes(bytes, spkiNode), + publicKeyAlgorithmOid, + publicKeyCurveName: curveNameForOid(publicKeyCurveOid), + }; +}; + +const parseAttribute = (bytes: Uint8Array, node: Asn1Node) => { + const [oidNode, valuesNode] = node.children; + if (!oidNode || !valuesNode || !isTag(oidNode, UNIVERSAL, TAG.oid)) { + return undefined; + } + return { + oid: decodeOid(bytes, oidNode), + values: valuesNode.children, + }; +}; + +const parseSigner = ( + bytes: Uint8Array, + node: Asn1Node, + index: number +): SignerRecord | undefined => { + if (!isTag(node, UNIVERSAL, TAG.sequence)) { + return undefined; + } + const children = node.children; + let cursor = 0; + const versionNode = children[cursor++]; + const sidNode = children[cursor++]; + const digestAlgorithmNode = children[cursor++]; + if (!versionNode || !sidNode || !digestAlgorithmNode) { + return undefined; + } + const signedAttributesNode = isTag(children[cursor], CONTEXT, 0) + ? children[cursor++] + : undefined; + const signatureAlgorithmNode = children[cursor++]; + const signatureNode = children[cursor++]; + const unsignedAttributesNode = isTag(children[cursor], CONTEXT, 1) + ? children[cursor] + : undefined; + + let issuerDer: string | undefined; + let serialDer: string | undefined; + let subjectKeyIdentifier: string | undefined; + let sid = 'Unknown signer identifier'; + if (isTag(sidNode, UNIVERSAL, TAG.sequence)) { + const issuerNode = sidNode.children[0]; + const serialNode = sidNode.children[1]; + if (issuerNode && serialNode) { + issuerDer = bytesToHex(fullBytes(bytes, issuerNode)); + serialDer = bytesToHex(positiveIntegerBytes(bytes, serialNode)); + sid = `${decodeName(bytes, issuerNode)} · ${serialDer}`; + } + } else if (isTag(sidNode, CONTEXT, 0)) { + subjectKeyIdentifier = bytesToHex(contentBytes(bytes, sidNode)); + sid = `Subject key identifier ${subjectKeyIdentifier}`; + } + + let signingTime: string | undefined; + let messageDigest: string | undefined; + let contentTypeOid: string | undefined; + let signingCertificateV2 = false; + for (const child of signedAttributesNode?.children || []) { + const attribute = parseAttribute(bytes, child); + const value = attribute?.values[0]; + if (!attribute || !value) { + continue; + } + if (attribute.oid === OID.signingTimeAttribute && ( + isTag(value, UNIVERSAL, TAG.utcTime) || isTag(value, UNIVERSAL, TAG.generalizedTime) + )) { + signingTime = decodeTime(bytes, value); + } else if (attribute.oid === OID.messageDigestAttribute && isTag(value, UNIVERSAL, TAG.octetString)) { + messageDigest = bytesToHex(contentBytes(bytes, value)); + } else if (attribute.oid === OID.contentTypeAttribute && isTag(value, UNIVERSAL, TAG.oid)) { + contentTypeOid = decodeOid(bytes, value); + } else if (attribute.oid === OID.signingCertificateV2Attribute) { + signingCertificateV2 = true; + } + } + + let signatureTimestampTokens = 0; + for (const child of unsignedAttributesNode?.children || []) { + const attribute = parseAttribute(bytes, child); + if (attribute?.oid === OID.signatureTimestampTokenAttribute) { + signatureTimestampTokens += attribute.values.length; + } + } + + const digestAlgorithmOid = algorithmOid(bytes, digestAlgorithmNode); + const signatureAlgorithmOid = algorithmOid(bytes, signatureAlgorithmNode); + return { + summary: { + index, + sid, + digestAlgorithmOid, + digestAlgorithm: formatOid(digestAlgorithmOid), + signatureAlgorithmOid, + signatureAlgorithm: formatOid(signatureAlgorithmOid), + signingTime, + messageDigest, + contentTypeOid, + signatureTimestampTokens, + signingCertificateV2, + }, + issuerDer, + serialDer, + subjectKeyIdentifier, + digestAlgorithmNode, + signatureAlgorithmNode, + signedAttributesNode, + signature: signatureNode && isTag(signatureNode, UNIVERSAL, TAG.octetString) + ? contentBytes(bytes, signatureNode) + : undefined, + }; +}; + +const parseCmsContentInfo = (bytes: Uint8Array, root: Asn1Node): CmsRecord => { + assertBounds(isTag(root, UNIVERSAL, TAG.sequence), 'CMS ContentInfo must be an ASN.1 SEQUENCE.'); + const oidNode = root.children[0]; + assertBounds(oidNode && isTag(oidNode, UNIVERSAL, TAG.oid), 'CMS ContentInfo is missing contentType.'); + const contentTypeOid = decodeOid(bytes, oidNode); + if (contentTypeOid !== OID.signedData) { + return { + contentTypeOid, + digestAlgorithmOids: [], + certificates: [], + crlCount: 0, + signers: [], + }; + } + const explicitContent = root.children.find(child => isTag(child, CONTEXT, 0)); + const signedData = explicitContent?.children[0]; + assertBounds(signedData && isTag(signedData, UNIVERSAL, TAG.sequence), 'CMS SignedData payload is missing.'); + const children = signedData.children; + const digestAlgorithmsNode = children[1]; + const encapContentInfo = children[2]; + assertBounds(digestAlgorithmsNode && encapContentInfo, 'CMS SignedData is truncated.'); + const digestAlgorithmOids = digestAlgorithmsNode.children + .map(child => algorithmOid(bytes, child)) + .filter((value): value is string => Boolean(value)); + const signedContentTypeNode = encapContentInfo.children[0]; + const signedContentTypeOid = signedContentTypeNode && isTag(signedContentTypeNode, UNIVERSAL, TAG.oid) + ? decodeOid(bytes, signedContentTypeNode) + : undefined; + const eContentNode = encapContentInfo.children.find(child => isTag(child, CONTEXT, 0)); + const embeddedContent = eContentNode ? collectOctetString(bytes, eContentNode) : undefined; + const certificatesNode = children.find(child => isTag(child, CONTEXT, 0)); + const crlsNode = children.find(child => isTag(child, CONTEXT, 1)); + const signerInfosNode = [...children].reverse().find(child => isTag(child, UNIVERSAL, TAG.set)); + const certificates = (certificatesNode?.children || []) + .map((child, index) => parseCertificate(bytes, child, index)) + .filter((value): value is CertificateRecord => Boolean(value)); + const signers = (signerInfosNode?.children || []) + .map((child, index) => parseSigner(bytes, child, index)) + .filter((value): value is SignerRecord => Boolean(value)); + return { + contentTypeOid, + signedContentTypeOid, + digestAlgorithmOids, + embeddedContent, + certificates, + crlCount: crlsNode?.children.length || 0, + signers, + }; +}; + +const subtleCrypto = () => globalThis.crypto?.subtle; + +const webCryptoBuffer = (bytes: Uint8Array): ArrayBuffer => { + const copy = new Uint8Array(bytes.byteLength); + copy.set(bytes); + return copy.buffer; +}; + +const digestNameForOid = (oid?: string): AlgorithmIdentifier | undefined => { + if (oid === OID.sha1) return 'SHA-1'; + if (oid === OID.sha256) return 'SHA-256'; + if (oid === OID.sha384) return 'SHA-384'; + if (oid === OID.sha512) return 'SHA-512'; + return undefined; +}; + +const digestBytes = async (oid: string | undefined, bytes: Uint8Array) => { + const subtle = subtleCrypto(); + const algorithm = digestNameForOid(oid); + if (!subtle || !algorithm) { + return undefined; + } + return new Uint8Array(await subtle.digest(algorithm, webCryptoBuffer(bytes))); +}; + +const pssSaltLength = (bytes: Uint8Array, algorithmNode: Asn1Node | undefined, hashOid?: string) => { + const defaultLength = hashOid === OID.sha512 ? 64 : hashOid === OID.sha384 ? 48 : hashOid === OID.sha1 ? 20 : 32; + const params = algorithmNode?.children[1]; + const saltContainer = params?.children.find(child => isTag(child, CONTEXT, 2)); + const saltNode = saltContainer?.children[0]; + return saltNode && isTag(saltNode, UNIVERSAL, TAG.integer) + ? integerNumber(bytes, saltNode) || defaultLength + : defaultLength; +}; + +const signatureHashOid = (signatureOid: string | undefined, digestOid: string | undefined) => { + if (signatureOid === OID.sha1WithRsa) return OID.sha1; + if (signatureOid === OID.sha256WithRsa || signatureOid === OID.ecdsaSha256) return OID.sha256; + if (signatureOid === OID.sha384WithRsa || signatureOid === OID.ecdsaSha384) return OID.sha384; + if (signatureOid === OID.sha512WithRsa || signatureOid === OID.ecdsaSha512) return OID.sha512; + return digestOid; +}; + +const signedAttributesBytes = (bytes: Uint8Array, node: Asn1Node) => { + const raw = new Uint8Array(fullBytes(bytes, node)); + if (raw.byteLength && raw[0] === 0xa0) { + raw[0] = 0x31; + } + return raw; +}; + +const ecdsaDerToRaw = (signature: Uint8Array, coordinateLength: number) => { + try { + const root = parseRoot(signature); + if (!isTag(root, UNIVERSAL, TAG.sequence) || root.children.length < 2) { + return signature; + } + const normalize = (node: Asn1Node) => { + let value = positiveIntegerBytes(signature, node); + if (value.byteLength > coordinateLength) { + value = value.subarray(value.byteLength - coordinateLength); + } + const output = new Uint8Array(coordinateLength); + output.set(value, coordinateLength - value.byteLength); + return output; + }; + const raw = new Uint8Array(coordinateLength * 2); + raw.set(normalize(root.children[0]!), 0); + raw.set(normalize(root.children[1]!), coordinateLength); + return raw; + } catch { + return signature; + } +}; + +const findSignerCertificate = (signer: SignerRecord, certificates: CertificateRecord[]) => { + if (signer.subjectKeyIdentifier) { + return certificates.find(certificate => certificate.subjectKeyIdentifier === signer.subjectKeyIdentifier); + } + if (signer.issuerDer && signer.serialDer) { + return certificates.find(certificate => ( + certificate.issuerDer === signer.issuerDer && certificate.serialDer === signer.serialDer + )); + } + return undefined; +}; + +const verifySigner = async ( + bytes: Uint8Array, + signer: SignerRecord, + certificates: CertificateRecord[], + content?: Uint8Array +) => { + const summary = signer.summary; + const digestOid = summary.digestAlgorithmOid; + if (content && summary.messageDigest) { + const digest = await digestBytes(digestOid, content); + if (digest) { + summary.digestMatches = equalBytes(digest, Uint8Array.from( + summary.messageDigest.match(/.{2}/g)?.map(value => Number.parseInt(value, 16)) || [] + )); + } + } + + const certificate = findSignerCertificate(signer, certificates); + if (!certificate) { + summary.verificationError = 'Matching signer certificate is not included.'; + return; + } + summary.certificateIndex = certificate.summary.index; + if (!signer.signature) { + summary.verificationError = 'Signature value is missing.'; + return; + } + const signedInput = signer.signedAttributesNode + ? signedAttributesBytes(bytes, signer.signedAttributesNode) + : content; + if (!signedInput) { + summary.verificationError = 'Original content is required to verify this signature.'; + return; + } + const subtle = subtleCrypto(); + if (!subtle) { + summary.verificationError = 'Web Crypto is unavailable.'; + return; + } + + const signatureOid = summary.signatureAlgorithmOid; + const hashOid = signatureHashOid(signatureOid, digestOid); + const hash = digestNameForOid(hashOid); + try { + if ( + certificate.publicKeyAlgorithmOid === OID.rsaEncryption || + signatureOid === OID.rsaEncryption || + signatureOid === OID.sha1WithRsa || + signatureOid === OID.sha256WithRsa || + signatureOid === OID.sha384WithRsa || + signatureOid === OID.sha512WithRsa + ) { + assertBounds(hash, `Unsupported RSA digest algorithm ${hashOid || 'unknown'}.`); + const algorithm: RsaHashedImportParams = { name: 'RSASSA-PKCS1-v1_5', hash }; + const key = await subtle.importKey('spki', webCryptoBuffer(certificate.spki), algorithm, false, ['verify']); + summary.cryptographicSignatureValid = await subtle.verify( + { name: 'RSASSA-PKCS1-v1_5' }, + key, + webCryptoBuffer(signer.signature), + webCryptoBuffer(signedInput) + ); + return; + } + if (signatureOid === OID.rsaPss) { + assertBounds(hash, `Unsupported RSA-PSS digest algorithm ${hashOid || 'unknown'}.`); + const algorithm: RsaHashedImportParams = { name: 'RSA-PSS', hash }; + const key = await subtle.importKey('spki', webCryptoBuffer(certificate.spki), algorithm, false, ['verify']); + summary.cryptographicSignatureValid = await subtle.verify( + { name: 'RSA-PSS', saltLength: pssSaltLength(bytes, signer.signatureAlgorithmNode, hashOid) }, + key, + webCryptoBuffer(signer.signature), + webCryptoBuffer(signedInput) + ); + return; + } + if ( + certificate.publicKeyAlgorithmOid === OID.ecPublicKey || + signatureOid === OID.ecdsaSha256 || + signatureOid === OID.ecdsaSha384 || + signatureOid === OID.ecdsaSha512 + ) { + assertBounds(hash, `Unsupported ECDSA digest algorithm ${hashOid || 'unknown'}.`); + assertBounds(certificate.publicKeyCurveName, 'Unsupported or missing EC named curve.'); + const key = await subtle.importKey( + 'spki', + webCryptoBuffer(certificate.spki), + { name: 'ECDSA', namedCurve: certificate.publicKeyCurveName }, + false, + ['verify'] + ); + const coordinateLength = certificate.publicKeyCurveName === 'P-521' + ? 66 + : certificate.publicKeyCurveName === 'P-384' + ? 48 + : 32; + summary.cryptographicSignatureValid = await subtle.verify( + { name: 'ECDSA', hash }, + key, + webCryptoBuffer(ecdsaDerToRaw(signer.signature, coordinateLength)), + webCryptoBuffer(signedInput) + ); + return; + } + if (certificate.publicKeyAlgorithmOid === OID.ed25519 || signatureOid === OID.ed25519) { + const key = await subtle.importKey('spki', webCryptoBuffer(certificate.spki), { name: 'Ed25519' }, false, ['verify']); + summary.cryptographicSignatureValid = await subtle.verify('Ed25519', key, webCryptoBuffer(signer.signature), webCryptoBuffer(signedInput)); + return; + } + summary.verificationError = `Unsupported signature algorithm ${signatureOid || 'unknown'}.`; + } catch (error) { + summary.verificationError = error instanceof Error ? error.message : String(error); + } +}; + +const enrichCertificateFingerprints = async (certificates: CertificateRecord[], bytes: Uint8Array) => { + const subtle = subtleCrypto(); + if (!subtle) { + return; + } + await Promise.all(certificates.map(async certificate => { + try { + const digest = new Uint8Array(await subtle.digest('SHA-256', webCryptoBuffer(fullBytes(bytes, certificate.node)))); + certificate.summary.fingerprintSha256 = bytesToHex(digest, ':'); + } catch { + // Fingerprints are presentation metadata; parsing remains useful without one. + } + })); +}; + +const parseMessageImprint = (bytes: Uint8Array, node: Asn1Node | undefined) => { + if (!node || !isTag(node, UNIVERSAL, TAG.sequence)) { + return {}; + } + const algorithm = node.children[0]; + const digest = node.children[1]; + const algorithmOidValue = algorithmOid(bytes, algorithm); + return { + algorithmOid: algorithmOidValue, + algorithm: formatOid(algorithmOidValue), + digest: digest && isTag(digest, UNIVERSAL, TAG.octetString) + ? bytesToHex(contentBytes(bytes, digest)) + : undefined, + }; +}; + +const parseAccuracy = (bytes: Uint8Array, node: Asn1Node) => { + const accuracy: TimestampAccuracySummary = {}; + for (const child of node.children) { + if (isTag(child, UNIVERSAL, TAG.integer)) { + accuracy.seconds = integerNumber(bytes, child); + } else if (isTag(child, CONTEXT, 0)) { + const value = child.children[0]; + accuracy.millis = value ? integerNumber(bytes, value) : Number.parseInt(bytesToHex(contentBytes(bytes, child)), 16); + } else if (isTag(child, CONTEXT, 1)) { + const value = child.children[0]; + accuracy.micros = value ? integerNumber(bytes, value) : Number.parseInt(bytesToHex(contentBytes(bytes, child)), 16); + } + } + return accuracy; +}; + +const decodeGeneralName = (bytes: Uint8Array, node: Asn1Node) => { + if (node.tagClass !== CONTEXT) { + return bytesToHex(contentBytes(bytes, node)); + } + if (node.tagNumber === 4 && node.children[0]) { + return decodeName(bytes, node.children[0]); + } + if ([1, 2, 6].includes(node.tagNumber)) { + return decodeAscii(contentBytes(bytes, node)); + } + if (node.tagNumber === 8) { + try { + return decodeOid(bytes, { ...node, tagClass: UNIVERSAL, tagNumber: TAG.oid }); + } catch { + return bytesToHex(contentBytes(bytes, node)); + } + } + return bytesToHex(contentBytes(bytes, node)); +}; + +const parseTstInfo = async ( + bytes: Uint8Array, + originalContent?: Uint8Array +): Promise => { + const root = parseRoot(bytes); + assertBounds(isTag(root, UNIVERSAL, TAG.sequence), 'RFC 3161 TSTInfo must be a SEQUENCE.'); + const children = root.children; + const versionNode = children[0]; + const policyNode = children[1]; + const imprintNode = children[2]; + const serialNode = children[3]; + const generationTimeNode = children[4]; + const imprint = parseMessageImprint(bytes, imprintNode); + const summary: TimestampInfoSummary = { + version: versionNode ? integerNumber(bytes, versionNode) : undefined, + policyOid: policyNode && isTag(policyNode, UNIVERSAL, TAG.oid) ? decodeOid(bytes, policyNode) : undefined, + serialNumber: serialNode ? integerHex(bytes, serialNode) : undefined, + generationTime: generationTimeNode ? decodeTime(bytes, generationTimeNode) : undefined, + messageImprintAlgorithmOid: imprint.algorithmOid, + messageImprintAlgorithm: imprint.algorithm, + messageImprint: imprint.digest, + }; + for (const child of children.slice(5)) { + if (isTag(child, UNIVERSAL, TAG.sequence) && summary.accuracy === undefined) { + summary.accuracy = parseAccuracy(bytes, child); + } else if (isTag(child, UNIVERSAL, TAG.boolean)) { + summary.ordering = decodeBoolean(bytes, child); + } else if (isTag(child, UNIVERSAL, TAG.integer) && !summary.nonce) { + summary.nonce = integerHex(bytes, child); + } else if (isTag(child, CONTEXT, 0)) { + const generalName = child.children[0] || child; + summary.tsa = decodeGeneralName(bytes, generalName); + } + } + if (originalContent && imprint.algorithmOid && imprint.digest) { + const digest = await digestBytes(imprint.algorithmOid, originalContent); + if (digest) { + summary.messageImprintMatchesOriginal = bytesToHex(digest) === imprint.digest; + } + } + return summary; +}; + +const parseTimestampRequest = async ( + bytes: Uint8Array, + root: Asn1Node, + originalContent?: Uint8Array +): Promise => { + const children = root.children; + const versionNode = children[0]; + const imprint = parseMessageImprint(bytes, children[1]); + const summary: TimestampInfoSummary = { + version: versionNode ? integerNumber(bytes, versionNode) : undefined, + messageImprintAlgorithmOid: imprint.algorithmOid, + messageImprintAlgorithm: imprint.algorithm, + messageImprint: imprint.digest, + }; + for (const child of children.slice(2)) { + if (isTag(child, UNIVERSAL, TAG.oid) && !summary.policyOid) { + summary.policyOid = decodeOid(bytes, child); + } else if (isTag(child, UNIVERSAL, TAG.integer) && !summary.nonce) { + summary.nonce = integerHex(bytes, child); + } else if (isTag(child, UNIVERSAL, TAG.boolean)) { + summary.certReq = decodeBoolean(bytes, child); + } + } + if (originalContent && imprint.algorithmOid && imprint.digest) { + const digest = await digestBytes(imprint.algorithmOid, originalContent); + if (digest) { + summary.messageImprintMatchesOriginal = bytesToHex(digest) === imprint.digest; + } + } + return summary; +}; + +const timestampStatusLabel = (status?: number) => { + const labels: Readonly> = { + 0: 'granted', + 1: 'grantedWithMods', + 2: 'rejection', + 3: 'waiting', + 4: 'revocationWarning', + 5: 'revocationNotification', + }; + return status === undefined ? undefined : labels[status] || `status-${status}`; +}; + +const decodeTimestampFailureInfo = (bytes: Uint8Array, node: Asn1Node | undefined) => { + if (!node || !isTag(node, UNIVERSAL, TAG.bitString)) return undefined; + const value = contentBytes(bytes, node); + if (value.byteLength < 2) return bytesToHex(value); + const unused = value[0] || 0; + const payload = value.subarray(1); + const names = [ + [0, 'badAlg'], [2, 'badRequest'], [5, 'badDataFormat'], [14, 'timeNotAvailable'], + [15, 'unacceptedPolicy'], [16, 'unacceptedExtension'], [17, 'addInfoNotAvailable'], + [25, 'systemFailure'], + ] as const; + const set: string[] = []; + for (const [bit, name] of names) { + const byteIndex = Math.floor(bit / 8); + const bitIndex = 7 - (bit % 8); + if (byteIndex < payload.byteLength && (payload[byteIndex]! & (1 << bitIndex))) set.push(name); + } + return set.length ? set.join(', ') : `0x${bytesToHex(payload)} (unused bits ${unused})`; +}; + +const parseTimestampResponse = (bytes: Uint8Array, root: Asn1Node) => { + const statusInfoNode = root.children[0]; + const tokenNode = root.children[1]; + assertBounds(statusInfoNode && isTag(statusInfoNode, UNIVERSAL, TAG.sequence), 'RFC 3161 response is missing PKIStatusInfo.'); + const statusNode = statusInfoNode.children[0]; + const status = statusNode ? integerNumber(bytes, statusNode) : undefined; + const stringsNode = statusInfoNode.children.find(child => isTag(child, UNIVERSAL, TAG.sequence)); + const failureNode = statusInfoNode.children.find(child => isTag(child, UNIVERSAL, TAG.bitString)); + return { + response: { + status, + statusLabel: timestampStatusLabel(status), + statusText: (stringsNode?.children || []).map(child => decodeString(bytes, child)), + failureInfo: decodeTimestampFailureInfo(bytes, failureNode), + } satisfies TimestampResponseSummary, + tokenNode, + }; +}; + +const looksLikeTimestampRequest = (root: Asn1Node) => { + const [version, imprint] = root.children; + return Boolean( + isTag(version, UNIVERSAL, TAG.integer) && + isTag(imprint, UNIVERSAL, TAG.sequence) && + isTag(imprint?.children[0], UNIVERSAL, TAG.sequence) && + isTag(imprint?.children[1], UNIVERSAL, TAG.octetString) + ); +}; + +const looksLikeTimestampResponse = (root: Asn1Node) => { + const statusInfo = root.children[0]; + return Boolean( + isTag(statusInfo, UNIVERSAL, TAG.sequence) && + isTag(statusInfo?.children[0], UNIVERSAL, TAG.integer) + ); +}; + +const cmsKind = (cms: CmsRecord): SignatureContainerKind => { + if (cms.contentTypeOid !== OID.signedData) { + return 'cms-content'; + } + if (cms.signedContentTypeOid === OID.tstInfo) { + return 'timestamp-token'; + } + if (!cms.signers.length && cms.certificates.length) { + return 'cms-certificates'; + } + return 'cms-signed-data'; +}; + +const detectedFormatForKind = (kind: SignatureContainerKind) => { + const labels: Readonly> = { + 'cms-signed-data': 'CMS / PKCS#7 SignedData', + 'cms-certificates': 'CMS / PKCS#7 certificate container', + 'cms-content': 'CMS / PKCS#7 content container', + 'timestamp-request': 'RFC 3161 TimeStampReq', + 'timestamp-response': 'RFC 3161 TimeStampResp', + 'timestamp-token': 'RFC 3161 TimeStampToken', + 'timestamped-data': 'RFC 5544 TimeStampedData', + unknown: 'Unknown cryptographic container', + }; + return labels[kind]; +}; + +const baseWarnings = () => [ + 'Cryptographic verification does not establish certificate trust, policy compliance, qualified status, or legal validity.', + 'Phase 1 covers CMS/PKCS#7, CAdES attributes, RFC 3161, and RFC 5544 TimeStampedData. OpenPGP, ASiC, evidence records, PAdES, XAdES, and JAdES are out of scope.', + 'No private keys, passphrases, trust stores, network lookups, or LGPL OpenPGP source are used or bundled.', +]; + +const inspectCmsNode = async ( + bytes: Uint8Array, + root: Asn1Node, + originalContent?: Uint8Array +): Promise => { + const cms = parseCmsContentInfo(bytes, root); + await enrichCertificateFingerprints(cms.certificates, bytes); + const verificationContent = cms.embeddedContent || originalContent; + await Promise.all(cms.signers.map(signer => verifySigner(bytes, signer, cms.certificates, verificationContent))); + const kind = cmsKind(cms); + const timestamp = cms.signedContentTypeOid === OID.tstInfo && cms.embeddedContent + ? await parseTstInfo(cms.embeddedContent, originalContent) + : undefined; + return { + kind, + detectedFormat: detectedFormatForKind(kind), + sourceSize: bytes.byteLength, + contentTypeOid: cms.contentTypeOid, + contentType: formatOid(cms.contentTypeOid), + signedContentTypeOid: cms.signedContentTypeOid, + signedContentType: formatOid(cms.signedContentTypeOid), + digestAlgorithms: cms.digestAlgorithmOids.map(formatOid), + embeddedContent: cms.embeddedContent, + detached: cms.contentTypeOid === OID.signedData && !cms.embeddedContent && cms.signers.length > 0, + requiresOriginalContent: ( + cms.contentTypeOid === OID.signedData && !cms.embeddedContent && cms.signers.length > 0 + ) || Boolean(timestamp?.messageImprint && originalContent === undefined), + originalContentSupplied: Boolean(originalContent), + certificates: cms.certificates.map(certificate => certificate.summary), + signers: cms.signers.map(signer => signer.summary), + timestamp, + crlCount: cms.crlCount, + warnings: baseWarnings(), + }; +}; + +const decodePemIfNeeded = (input: Uint8Array) => { + const head = new TextDecoder('ascii', { fatal: false }).decode(input.subarray(0, Math.min(input.byteLength, 128))); + if (!head.includes('-----BEGIN ')) return input; + const text = new TextDecoder('ascii', { fatal: false }).decode(input); + const match = text.match(/-----BEGIN (?:PKCS7|CMS)-----([\s\S]*?)-----END (?:PKCS7|CMS)-----/); + assertBounds(match, 'Unsupported PEM cryptographic container. Expected PKCS7 or CMS armor.'); + const compact = match[1]!.replace(/\s+/g, ''); + assertBounds(compact.length > 0 && compact.length % 4 === 0, 'Malformed PEM base64 payload.'); + const binary = typeof atob === 'function' + ? atob(compact) + : (() => { throw new Error('PEM decoding requires a browser with atob support.'); })(); + const output = new Uint8Array(binary.length); + for (let index = 0; index < binary.length; index += 1) output[index] = binary.charCodeAt(index); + return output; +}; + +const findCmsContentInfoNode = (bytes: Uint8Array, node: Asn1Node, depth = 0): Asn1Node | undefined => { + if (depth > 12) return undefined; + if (isTag(node, UNIVERSAL, TAG.sequence)) { + const first = node.children[0]; + if (first && isTag(first, UNIVERSAL, TAG.oid) && decodeOid(bytes, first) === OID.signedData) return node; + } + for (const child of node.children) { + const found = findCmsContentInfoNode(bytes, child, depth + 1); + if (found) return found; + } + return undefined; +}; + +const parseTimestampedData = async ( + bytes: Uint8Array, + root: Asn1Node, + originalContent?: Uint8Array +): Promise => { + const content = root.children.find(child => isTag(child, CONTEXT, 0))?.children[0]; + assertBounds(content && isTag(content, UNIVERSAL, TAG.sequence), 'RFC 5544 TimeStampedData payload is missing.'); + const fields = content.children; + let cursor = 0; + const versionNode = fields[cursor++]; + assertBounds(versionNode && isTag(versionNode, UNIVERSAL, TAG.integer), 'RFC 5544 TimeStampedData version is missing.'); + let dataUri: string | undefined; + if (isTag(fields[cursor], UNIVERSAL, TAG.ia5String)) dataUri = decodeString(bytes, fields[cursor++]!); + let filename: string | undefined; + let mediaType: string | undefined; + let hashProtected: boolean | undefined; + const metadataNode = fields[cursor] && isTag(fields[cursor], UNIVERSAL, TAG.sequence) ? fields[cursor++] : undefined; + if (metadataNode) { + const metadata = metadataNode.children; + if (metadata[0] && isTag(metadata[0], UNIVERSAL, TAG.boolean)) hashProtected = decodeBoolean(bytes, metadata[0]); + for (const child of metadata.slice(1)) { + if (!filename && (isTag(child, UNIVERSAL, TAG.utf8String) || isTag(child, UNIVERSAL, TAG.printableString))) filename = decodeString(bytes, child); + else if (!mediaType && isTag(child, UNIVERSAL, TAG.ia5String)) mediaType = decodeString(bytes, child); + } + } + const embeddedNode = fields[cursor] && isTag(fields[cursor], UNIVERSAL, TAG.octetString) ? fields[cursor++] : undefined; + const embeddedContent = embeddedNode ? collectOctetString(bytes, embeddedNode) : undefined; + const evidenceNode = fields[cursor]; + const cmsNode = evidenceNode ? findCmsContentInfoNode(bytes, evidenceNode) : undefined; + const evidenceInspection = cmsNode ? await inspectCmsNode(bytes, cmsNode, embeddedContent || originalContent) : undefined; + const evidenceEntries = evidenceNode?.children.length || 0; + return { + kind: 'timestamped-data', + detectedFormat: detectedFormatForKind('timestamped-data'), + sourceSize: bytes.byteLength, + contentTypeOid: OID.timestampedData, + contentType: formatOid(OID.timestampedData), + digestAlgorithms: evidenceInspection?.digestAlgorithms || [], + embeddedContent, + detached: !embeddedContent, + requiresOriginalContent: !embeddedContent && Boolean(dataUri), + originalContentSupplied: Boolean(originalContent), + certificates: evidenceInspection?.certificates || [], + signers: evidenceInspection?.signers || [], + timestamp: evidenceInspection?.timestamp, + crlCount: evidenceInspection?.crlCount || 0, + timestampedData: { dataUri, filename, mediaType, hashProtected, temporalEvidenceEntries: evidenceEntries }, + warnings: [ + ...baseWarnings(), + ...(dataUri ? ['External RFC 5544 dataUri values are displayed as untrusted metadata and are never fetched automatically.'] : []), + ], + }; +}; + +export const inspectSignatureContainer = async ( + input: ArrayBuffer | Uint8Array, + options: InspectSignatureOptions = {} +): Promise => { + const rawBytes = toUint8Array(input); + const bytes = decodePemIfNeeded(rawBytes); + assertBounds(bytes.byteLength > 0, 'The cryptographic container is empty.'); + const originalContent = options.originalContent + ? toUint8Array(options.originalContent) + : undefined; + const root = parseRoot(bytes); + assertBounds(isTag(root, UNIVERSAL, TAG.sequence), 'Unsupported cryptographic container: ASN.1 root is not a SEQUENCE.'); + + const first = root.children[0]; + if (first && isTag(first, UNIVERSAL, TAG.oid)) { + const contentTypeOid = decodeOid(bytes, first); + if (contentTypeOid === OID.timestampedData) { + return parseTimestampedData(bytes, root, originalContent); + } + return inspectCmsNode(bytes, root, originalContent); + } + + if (looksLikeTimestampRequest(root)) { + const timestamp = await parseTimestampRequest(bytes, root, originalContent); + return { + kind: 'timestamp-request', + detectedFormat: detectedFormatForKind('timestamp-request'), + sourceSize: bytes.byteLength, + digestAlgorithms: timestamp.messageImprintAlgorithm ? [timestamp.messageImprintAlgorithm] : [], + detached: true, + requiresOriginalContent: Boolean(timestamp.messageImprint && !originalContent), + originalContentSupplied: Boolean(originalContent), + certificates: [], + signers: [], + timestamp, + crlCount: 0, + warnings: baseWarnings(), + }; + } + + if (looksLikeTimestampResponse(root)) { + const { response, tokenNode } = parseTimestampResponse(bytes, root); + if (tokenNode) { + const tokenInspection = await inspectCmsNode(bytes, tokenNode, originalContent); + return { + ...tokenInspection, + kind: 'timestamp-response', + detectedFormat: detectedFormatForKind('timestamp-response'), + timestampResponse: response, + }; + } + return { + kind: 'timestamp-response', + detectedFormat: detectedFormatForKind('timestamp-response'), + sourceSize: bytes.byteLength, + digestAlgorithms: [], + detached: true, + requiresOriginalContent: false, + originalContentSupplied: Boolean(originalContent), + certificates: [], + signers: [], + timestampResponse: response, + crlCount: 0, + warnings: baseWarnings(), + }; + } + + return { + kind: 'unknown', + detectedFormat: detectedFormatForKind('unknown'), + sourceSize: bytes.byteLength, + digestAlgorithms: [], + detached: false, + requiresOriginalContent: false, + originalContentSupplied: Boolean(originalContent), + certificates: [], + signers: [], + crlCount: 0, + warnings: baseWarnings(), + }; +}; + +export const signatureOidLabels = OID_LABELS; diff --git a/packages/renderers/signature/src/workerProtocol.ts b/packages/renderers/signature/src/workerProtocol.ts new file mode 100644 index 000000000..d2fbbe6b1 --- /dev/null +++ b/packages/renderers/signature/src/workerProtocol.ts @@ -0,0 +1,24 @@ +import type { + OpenPgpInspectionResult, + OpenPgpVerificationResult, + OpenPgpWorkerError, + SignatureParseLimits, +} from './openpgp/types.js'; + +export type SignatureWorkerRequest = + | { id: string; type: 'classify'; input: ArrayBuffer; limits: SignatureParseLimits } + | { id: string; type: 'inspect'; input: ArrayBuffer; limits: SignatureParseLimits } + | { + id: string; + type: 'verify-detached'; + content: ArrayBuffer; + signature: ArrayBuffer; + publicKeys: ArrayBuffer[]; + limits: SignatureParseLimits; + }; + +export type SignatureWorkerResponse = + | { id: string; ok: true; type: 'classify'; result: OpenPgpInspectionResult } + | { id: string; ok: true; type: 'inspect'; result: OpenPgpInspectionResult } + | { id: string; ok: true; type: 'verify-detached'; result: OpenPgpVerificationResult } + | { id: string; ok: false; error: OpenPgpWorkerError }; diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/GITHUB-COMMENT.md b/packages/renderers/signature/test/fixtures/github-206-contributed/GITHUB-COMMENT.md new file mode 100644 index 000000000..4a74c9d7c --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/GITHUB-COMMENT.md @@ -0,0 +1,20 @@ +I prepared and attached a synthetic, non-secret fixture pack for the first +CMS/timestamp phase. + +It includes: + +- CMS SignedData in DER and PEM form; +- encapsulated PDF and detached PDF signature with matching and altered originals; +- a CAdES-BES fixture containing `signingCertificateV2`; +- two-signer and nested/repeated-signature cases; +- certificates-only PKCS#7 containers, including one with a CRL and one with a + deliberately misleading `.p7m` extension; +- RFC 3161 `.tsq`, `.tsr`, and `.tst` fixtures with SHA-256/SHA-384, nonce + present/absent, granted and rejected responses; +- RFC 5544 `.tsd` fixtures with embedded content and external content via a + non-resolvable `example.invalid` URI; +- truncated and wrong-content negative cases. + +All documents and identities are synthetic. The root CA is test-only and +untrusted, no private keys are included, and the fixtures are marked CC0-1.0. +The included `scripts/verify_all.sh` passes with OpenSSL 3.5.5. diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/LICENSE.txt b/packages/renderers/signature/test/fixtures/github-206-contributed/LICENSE.txt new file mode 100644 index 000000000..f5e029792 --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/LICENSE.txt @@ -0,0 +1,8 @@ +SPDX-License-Identifier: CC0-1.0 + +These files were generated from synthetic content solely as interoperability +and parser fixtures. They contain no third-party documents and no production +credentials. To the extent legally possible, the fixture author has waived +all copyright and related rights under CC0 1.0 Universal. + +The certificates are test-only and untrusted. No private keys are included. diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/README.md b/packages/renderers/signature/test/fixtures/github-206-contributed/README.md new file mode 100644 index 000000000..f04ca6269 --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/README.md @@ -0,0 +1,92 @@ +# File Viewer issue #206 - non-secret CMS and timestamp fixtures + +This package is intended for the first implementation phase requested in +`flyfish-dev/file-viewer#206`: CMS/PKCS#7/CAdES-adjacent container inspection +and RFC 3161 / RFC 5544 timestamp inspection. Every document and identity is +synthetic. The root CA is intentionally untrusted, and no private key is +included. + +## Best first fixtures + +| Fixture | Expected interpretation | Main behavior to test | +|---|---|---| +| `cms/invoice-encapsulated.pdf.p7m` | CMS SignedData, DER, one RSA signer, encapsulated PDF | Parse metadata, verify signature, extract and nested-preview the PDF | +| `cms/invoice-cades-bes.pdf.p7m` | CAdES-BES SignedData with `signingCertificateV2` | Detect CAdES attribute, verify, extract and preview the PDF | +| `cms/invoice-encapsulated.cmsc` | Same DER CMS bytes under `.cmsc` | Route by content and support the alias extension | +| `cms/invoice-detached.pdf.p7s` + `originals/invoice.pdf` | CMS detached signature | Ask for original; matching original verifies | +| same `.p7s` + `originals/invoice-tampered.pdf` | Negative detached-signature case | Digest/signature comparison must fail clearly | +| `cms/xml-encapsulated-but-named.p7s` | Encapsulated SignedData despite `.p7s` | Detect structure from content, not extension | +| `cms/hello-multi-signer.p7m` | Two signers: RSA/SHA-384 and ECDSA/SHA-384 | Display and verify every signer | +| `cms/invoice-double-signed.p7m` | Nested SignedData | Verify/extract outer P7M, then verify/extract inner PDF | +| `cms/certificate-chain.p7b` | Certificates-only PKCS#7 | Show certificates; do not label it a signed document | +| `cms/certificate-chain-with-crl.p7c` | Certificates-only PKCS#7 plus CRL | Show certificates and included CRL | +| `cms/certificate-only-misnamed.p7m` | Same certificates-only content with misleading extension | Content detection must override extension | +| `timestamps/invoice-sha256.tsq` | RFC 3161 request with nonce and `certReq` | Show imprint, algorithm, nonce, policy request fields | +| `timestamps/invoice-sha256.tsr` | Granted RFC 3161 response | Show status and contained token; match `invoice.pdf` | +| `timestamps/invoice-sha256.tst` | Standalone RFC 3161 token | Show TSA, generation time, policy, serial, imprint and certs | +| `timestamps/hello-sha384-no-nonce.*` | SHA-384 request/response/token without nonce | Correctly handle optional nonce absence | +| `timestamps/invoice-embedded.tsd` | RFC 5544 TimeStampedData with embedded PDF | Extract PDF, inspect temporal evidence, nested-preview PDF | +| `timestamps/invoice-external-content.tsd` | RFC 5544 TimeStampedData with `dataUri`, no content | Never fetch automatically; request supplied original | +| `timestamps/rejected-bad-alg.tsr` | RFC 3161 rejection, no token | Show status text and `badAlg` failure information | +| `negative/truncated-signed-data.p7m` | Truncated DER | Bounded, user-friendly parse error | +| `negative/plain-text-with-p7s-extension.p7s` | Not CMS at all | Reject by content detection | + +## Public trust and legal-validity expectations + +A successful cryptographic check means only that the bytes match the signature +or message imprint and that the test signature verifies under the supplied test +root. The viewer should separately report that this synthetic root is not in a +system trust store and should not imply legal validity. + +## Verification commands + +From the package root: + +```bash +./scripts/verify_all.sh +``` + +Representative commands: + +```bash +# Extract and verify encapsulated CMS +openssl cms -verify -binary -inform DER \ + -in cms/invoice-encapsulated.pdf.p7m \ + -CAfile certificates/test-root-ca.pem -purpose any \ + -out /tmp/extracted-invoice.pdf + +# Verify CAdES-BES and its signing-certificate attribute +openssl cms -verify -cades -binary -inform DER \ + -in cms/invoice-cades-bes.pdf.p7m \ + -CAfile certificates/test-root-ca.pem -purpose any \ + -out /tmp/extracted-cades-invoice.pdf + +# Verify detached CMS +openssl cms -verify -binary -inform DER \ + -in cms/invoice-detached.pdf.p7s \ + -content originals/invoice.pdf \ + -CAfile certificates/test-root-ca.pem -purpose any \ + -out /dev/null + +# Inspect certificates-only PKCS#7 +openssl pkcs7 -inform DER -in cms/certificate-chain-with-crl.p7c \ + -print_certs -text -noout + +# Inspect and verify RFC 3161 response +openssl ts -reply -in timestamps/invoice-sha256.tsr -text +openssl ts -verify -data originals/invoice.pdf \ + -in timestamps/invoice-sha256.tsr \ + -CAfile certificates/test-root-ca.pem \ + -untrusted certificates/test-tsa.pem +``` + +## Generation profile + +- Synthetic root CA: RSA 2048, SHA-256, CA=true. +- CMS Alice: RSA 2048; Bob: ECDSA P-256. +- TSA: RSA 2048; critical EKU contains only `timeStamping`. +- CMS fixtures use DER except the explicitly named PEM variant. +- Timestamp queries/responses/tokens are DER. +- RFC 5544 fixtures use `id-ct-timestampedData` (`1.2.840.113549.1.9.16.1.31`). +- TSD metadata has `hashProtected = FALSE`; therefore the first timestamp token + covers the content octets, not the metadata. diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/SHA256SUMS b/packages/renderers/signature/test/fixtures/github-206-contributed/SHA256SUMS new file mode 100644 index 000000000..8b467bebb --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/SHA256SUMS @@ -0,0 +1,42 @@ +93ea38d2ec6013bcd176cccef1106780a79c4ed69073d9c2ae69698f4fc71bd0 ./GITHUB-COMMENT.md +9a0bbcd8a4300841b733fc372258e2fc665fbd8fa79c12686e72877fc1c3bea8 ./LICENSE.txt +a37d58b3dbe5d935340bee7472e028a97c7ecd110f40439b88ec0a00d9b520e5 ./README.md +9cad8aa508a8367d12cee61abd7d17ca6ffba21085a6df6908b0bf1f9c38b13f ./certificates/cms-alice.pem +d85f70bde73056390e010574b99c445783c0829618cdb30430a1f949eb5a8dde ./certificates/cms-bob.pem +8d77d368501b2fc8d0f0fb1815f9749db012f0c13d52368ed4e012c3c1609d0e ./certificates/test-root-ca.crl.pem +1b500008b39ed75953d43b45b7f5a6daa5d43a16f700369c19273d89eb782938 ./certificates/test-root-ca.pem +14c880b22267ff1a193c61a1c1ca10f14f1800fe30023ec925821d52368be305 ./certificates/test-tsa.pem +0f52e417e472f825ec033c35a255bb979b41bb52037184a45abdcb73fc6ed370 ./cms/certificate-chain-with-crl.p7c +d73b0da7ad6b6de8e808def9d0e7017cb0edb82fcc5b926c9ad741990ebd2506 ./cms/certificate-chain.p7b +d73b0da7ad6b6de8e808def9d0e7017cb0edb82fcc5b926c9ad741990ebd2506 ./cms/certificate-only-misnamed.p7m +36f6b634ded1d2af194ed3deba8e6ac65af29ddb3298c22ca4b5a84adffec666 ./cms/hello-multi-signer.p7m +747e55a36957af196d38eee2ae7c6d7a966404bcb33714e68523d122916c9fa8 ./cms/invoice-cades-bes.pdf.p7m +91c1bb65dcc603d46ad765586327d447808398558f4eede70545d7b0286f6a79 ./cms/invoice-detached.pdf.p7s +77036fca19671817b240be1e318409c07ff277cdef85eb0cc677d600969f21a2 ./cms/invoice-double-signed.p7m +641720525382e95b6cdfcd4b4ef0b5137117ef7c6833c39bc33e8103b7daf120 ./cms/invoice-encapsulated.cmsc +641720525382e95b6cdfcd4b4ef0b5137117ef7c6833c39bc33e8103b7daf120 ./cms/invoice-encapsulated.pdf.p7m +660c41b27829709e034b81e7ae4a0657aba126e48e0778dea4c174c32b89170c ./cms/invoice-encapsulated.pem.pkcs7 +d6d958b1e27355d0b249ad9eca7aab0ece0f2182e92c8ab7abaa76b7a5da7d6f ./cms/xml-encapsulated-but-named.p7s +0c4d302be547776e5656d4636247fe6dae03344f233c10b939053d79b577ffe5 ./manifest.json +8135b759b283a228569052e7e6e3d1b4969c86d9d6eda4591b2e955634c36a88 ./negative/plain-text-with-p7s-extension.p7s +cc832c8f40aa023199ea4b5200ca8199c98df6c3df10a35365ccd9e136426f12 ./negative/truncated-signed-data.p7m +47b6d56ca56dd3dd9a3159890d705d1535d509e13e906fe6d45a6ac5e888e211 ./originals/hello-tampered.txt +8606fa0c1c69e9b2580e829157f62c0aa02f857458b20c64da411ffe27728f00 ./originals/hello.txt +6f812d14a4c1b7bebf3c1b1e626d9d6f98b91822fe4e2a59d23e2c6d3ee2d87e ./originals/invoice-tampered.pdf +26d6a9e51494ecb6b78016740fbab73eeec83effe4cc96f0db092f4b281e5f4c ./originals/invoice.pdf +3f7e32e943d6701f2e4b5302ded523d8118361835add1ae8c64e6366493d08e5 ./originals/sample.xml +a6aa74eb885a3766773786579731048426e9b5852225aaaae1e9cc21911c639d ./reports/openssl-inspection.txt +009915c3004c537d8424032b516232ee436f03d5b542fccef2236132d81e276a ./reports/verification.txt +4edc620467f7ed4c3da4421f1ea0a7e7d51e5ee145e8c33ef83ee0b17521283f ./scripts/REBUILDING.md +6d28808a2631f95f224a3107a044dd1e928697d3dffb7b7b09ecbe91f7395223 ./scripts/build_structured_timestamp_fixtures.py +fc64d0272168cd42d9c653edd9d564ea91bc8046f8ccb4c7860e5f5d7dd739d7 ./scripts/verify_all.sh +3c0a23fa6ffb4b1642f0eec68eb50414891645caeda64dd22039ffa8d5d1f6ef ./scripts/verify_tsd.py +c65857d7cd79f3edbeb074fe7b3244b941f8119438589e18b6e20deeface9cbb ./timestamps/hello-sha384-no-nonce.tsq +6f7fdd797901afa0033101a5cac94defe7c7db54006c3a057d71469ded28b673 ./timestamps/hello-sha384-no-nonce.tsr +45bb439bfea4a946d95732e29301bab5b561eb8b25facbe3b4b114f7d152ce7c ./timestamps/hello-sha384-no-nonce.tst +6b69e11108c23f1ea716ca3c0886174c07e91b955574f1f62dd3f69580ad0ff8 ./timestamps/invoice-embedded.tsd +a154ac28f4d9ff9347fe684a49c90cf4029878bb8f773760ead33f4653f821aa ./timestamps/invoice-external-content.tsd +24a7220f78c0eac243ac22530579b93774e8488d4f8076386b8adcc7953b369c ./timestamps/invoice-sha256.tsq +171927dcfb655a807c6a631e37814b652de472bfb90b8b55d2d178b9bbc42104 ./timestamps/invoice-sha256.tsr +e80bace1165b0fcc5e22a998117e861db577c62696eaea054d284200734b65ff ./timestamps/invoice-sha256.tst +e2e86363ef52241e8eea19d311a70dd35cdad53db2f136e48e83638ae6daab92 ./timestamps/rejected-bad-alg.tsr diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/cms-alice.pem b/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/cms-alice.pem new file mode 100644 index 000000000..190cbbb98 --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/cms-alice.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEBjCCAu6gAwIBAgIDAgahMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAklU +MSIwIAYDVQQKDBlGaWxlIFZpZXdlciBUZXN0IEZpeHR1cmVzMRUwEwYDVQQLDAxU +ZXN0aW5nIE9ubHkxHzAdBgNVBAMMFklzc3VlIDIwNiBUZXN0IFJvb3QgQ0EwHhcN +MjYwODIyMTAxNjQ4WhcNNDYwODE3MTAxNjQ4WjCBjjELMAkGA1UEBhMCSVQxIjAg +BgNVBAoMGUZpbGUgVmlld2VyIFRlc3QgRml4dHVyZXMxFTATBgNVBAsMDFRlc3Rp +bmcgT25seTEeMBwGA1UEAwwVQ01TIFRlc3QgU2lnbmVyIEFsaWNlMSQwIgYJKoZI +hvcNAQkBFhVhbGljZUBleGFtcGxlLmludmFsaWQwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDGMntZpJgU/xKD5udJ37O15u4rwXszMWc94fTJUnu1b25A +qXs06nXqCwSigcUgfmuVYc/XATgFWinXODBHbsorr9JaJf3RllqKNA7iYTXecizk +jF7JfdpGhpSfivYHAmHP5FmMUxBaojO2BQYnlFQaqA+bgKFDlKJWqfySukJIu6Pf +2xQQzMDvQCFlDsQM47b3U0vQ/k4kJuGnjHwEOWRSexQ0KaqA8pFYL/KBtUYj8ubU +Z2VuNVbTFwchYKryIeCPrJssZzYa43LD+AXhmiivDvHNR6iIH24CU+79Grnk0+Pf +yzz1YceL6fIo8ybtEFnSSiaVzjRSV6z3ULpP+EW5AgMBAAGjgZAwgY0wDAYDVR0T +AQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwQwHQYD +VR0OBBYEFNaTsC/q7UbtqhybRuu54KFWbyZhMB8GA1UdIwQYMBaAFJO8Cw+Yfy2A +4Yy/XfRT+e0BqSzPMBgGA1UdIAQRMA8wDQYLKwYBBAGDsgOBTgowDQYJKoZIhvcN +AQELBQADggEBAGZ/noQdv/IBOKlSd27pFVP9Es1gE23MAcYl4YjFSHoxXbuBiJjp +EHRxSAZj0xQAIdPB20kgqof4cWygnjQj1/ol2bJ1bM4ptG6IyiYr0g423366WBFz +SYP6jawxbtL/hKtLcjYL4I8qEz8MtqWgTjmo1hHal+O9eQ4dMTdDf5Brw46bZbZ3 +uxI8CfUowbcrrlBwBC3EgSY0zFexkOFr+PNcFuu6ghPkCgt8Bqtl1l7hUCw8w8Ln +CJeCX59WgA93spz3+RS5ZyZZjnfoRDf+9UfIDog1rForFaV6GUjKPTwKnljvuWL7 +Da/RL83KnxgMZ3rzpXQRMi2O76XQrjfMOX8= +-----END CERTIFICATE----- diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/cms-bob.pem b/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/cms-bob.pem new file mode 100644 index 000000000..df8b6ad3c --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/cms-bob.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNzCCAh+gAwIBAgIDAgayMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAklU +MSIwIAYDVQQKDBlGaWxlIFZpZXdlciBUZXN0IEZpeHR1cmVzMRUwEwYDVQQLDAxU +ZXN0aW5nIE9ubHkxHzAdBgNVBAMMFklzc3VlIDIwNiBUZXN0IFJvb3QgQ0EwHhcN +MjYwODIyMTAxNjQ4WhcNNDYwODE3MTAxNjQ4WjCBijELMAkGA1UEBhMCSVQxIjAg +BgNVBAoMGUZpbGUgVmlld2VyIFRlc3QgRml4dHVyZXMxFTATBgNVBAsMDFRlc3Rp +bmcgT25seTEcMBoGA1UEAwwTQ01TIFRlc3QgU2lnbmVyIEJvYjEiMCAGCSqGSIb3 +DQEJARYTYm9iQGV4YW1wbGUuaW52YWxpZDBZMBMGByqGSM49AgEGCCqGSM49AwEH +A0IABDkbcGXg+OCdgNQrV2N/oRZe9hEdTbdrjlMLdJQL7pYrfHsqAqs4HPolOec7 +iP8RLmMVcWZnoSDEmZFERyDGURGjgZAwgY0wDAYDVR0TAQH/BAIwADAOBgNVHQ8B +Af8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwQwHQYDVR0OBBYEFAg23hE4AMCD +4qlPQRODRxL8LEjaMB8GA1UdIwQYMBaAFJO8Cw+Yfy2A4Yy/XfRT+e0BqSzPMBgG +A1UdIAQRMA8wDQYLKwYBBAGDsgOBTgowDQYJKoZIhvcNAQELBQADggEBAKlIhPGZ +ALjTZXPUbniJsvxdDT0ZU4KEAV5OvGP5MxZrhHgp/kjB5P/CHGuM+78JTpNdQuBy +Cz/taCtQIqGROl8FzHd+JZmVX+grgiQ3PabcFBwoNL4YJzktAnSNIO+kq7Dv2Bvq +69rqAuEDB81kBraNI1MCa6Sbny1SIswozzUvF1+XfzEaaiUfl9/BK4l5+i/A07oQ +DfKEbGUveBbbb9qE+fZtr2r6ozjXF0/qJXJp/UjFs+HAHMB9LWiatjg5dM0EmpFh +p/vUzEJVKdxeR+mhYrnLh7G5KPd1gerohAjvabcImfwwURVUBj/Y74i3dp8iACfp +xcFRplQxcE7Ytg8= +-----END CERTIFICATE----- diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/test-root-ca.crl.pem b/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/test-root-ca.crl.pem new file mode 100644 index 000000000..66b5df533 --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/test-root-ca.crl.pem @@ -0,0 +1,13 @@ +-----BEGIN X509 CRL----- +MIIB5DCBzQIBATANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJJVDEiMCAGA1UE +CgwZRmlsZSBWaWV3ZXIgVGVzdCBGaXh0dXJlczEVMBMGA1UECwwMVGVzdGluZyBP +bmx5MR8wHQYDVQQDDBZJc3N1ZSAyMDYgVGVzdCBSb290IENBFw0yNjA4MjIxMDE2 +NDhaFw00NjA4MTcxMDE2NDhaoDAwLjAfBgNVHSMEGDAWgBSTvAsPmH8tgOGMv130 +U/ntAakszzALBgNVHRQEBAICEAAwDQYJKoZIhvcNAQELBQADggEBAAN6HOGLSnrZ +X3JoXHI+2vR3eZa35z13H5pB+oEZBA1C41Hqn8zOXoXsLNfan1xOqbUHBeDFibnl +upf01PmTYGZul26+5F7ALXssK3QRy6bbzi10z7ZLqbKi5krEGprH2C/XCaW06xdl +ZhiAfiJvFij40Sna1fZrba0a0I1ZjYiTODNzoH7yhUGYu2JPln8Ba5RXeCQT/ZSE +NQ3JIcVfQa4NtzJzAkm9+XQQOgBP23bdBLPXRL6MFK46ZdOPLFml2WrFBZBFBXDk +JbFp8J6FqPhX/BvN9nqGum9KGPnv6ctpI5MhJNnfXRtWBAeO2FwHLA2ZLiOn9Dqb +1Kn0hgJT8R8= +-----END X509 CRL----- diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/test-root-ca.pem b/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/test-root-ca.pem new file mode 100644 index 000000000..d28601ac1 --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/test-root-ca.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDxjCCAq6gAwIBAgIUM3G7+wOL77qYdrnvvB2ZBZqvKuUwDQYJKoZIhvcNAQEL +BQAwaTELMAkGA1UEBhMCSVQxIjAgBgNVBAoMGUZpbGUgVmlld2VyIFRlc3QgRml4 +dHVyZXMxFTATBgNVBAsMDFRlc3RpbmcgT25seTEfMB0GA1UEAwwWSXNzdWUgMjA2 +IFRlc3QgUm9vdCBDQTAeFw0yNjA4MjIxMDE2NDhaFw00NjA4MjIxMDE2NDhaMGkx +CzAJBgNVBAYTAklUMSIwIAYDVQQKDBlGaWxlIFZpZXdlciBUZXN0IEZpeHR1cmVz +MRUwEwYDVQQLDAxUZXN0aW5nIE9ubHkxHzAdBgNVBAMMFklzc3VlIDIwNiBUZXN0 +IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvS14Mjtjl +XeEiYVQUH84zNPzoPTjBc6yyZc/pvRbJmyMcBzsIXRk8vNZSuVK2365a49aMnoyC +Yn2VZHk/6PW/IysWGNs9ubonMQuE0gGXcH2BWGdy9kfpyxlT5GlJpOP5B6dVvsNB +wLUTA8esPS/EiQ5UdnP9SonDKfBwF02De2+3EBDfPhb923OPm0ZPi9kw7ZibO9oi +M9TkN/zp7g2jKwozXS7ojLaXRlpTC+1h/zWEZHTDWXDwS6U/sOsxQY/Z7wJVyOox +97ygyCXx/UEKxuP/GViAzLonXP+zQcRnM2DlKntBWXCIGzT05mjUFmtC5nJ8r6ZL +IA2dWY9r5sLbAgMBAAGjZjBkMB8GA1UdIwQYMBaAFJO8Cw+Yfy2A4Yy/XfRT+e0B +qSzPMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQW +BBSTvAsPmH8tgOGMv130U/ntAakszzANBgkqhkiG9w0BAQsFAAOCAQEAXQuqa5Il +uca9TAYgERJ1ZfkhtPQhBsQmJsI+N8rMLm2DshW4m8JH2aBHesSpp5AMMCoMcMUS +wqb6AXS1keRlm7emjpPbartrDLv174M667okmo+28YL4x0EsPZkUlq6Cg74pK4dd +lmb95ekJcCnrLqEBBEJ/hYHbzBiPtqlkOtw+l5P+lDaQIY4ntsch6/kPxM8EeYSi +ERaLFpyuryDBCwje9laEXWFE/jgcJPSWjqwZk2xPosjQpm4+oB6f+BzndNhbqVVR +QyV0F3daVpeyCYSP9O17tEds94fE2j9dFvPoByh96gmT0g1Dr4qc5rnabXaYErWK +MWsshPZ5IEoIlg== +-----END CERTIFICATE----- diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/test-tsa.pem b/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/test-tsa.pem new file mode 100644 index 000000000..8c27be8c8 --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/certificates/test-tsa.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID3zCCAsegAwIBAgIDAgZ6MA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAklU +MSIwIAYDVQQKDBlGaWxlIFZpZXdlciBUZXN0IEZpeHR1cmVzMRUwEwYDVQQLDAxU +ZXN0aW5nIE9ubHkxHzAdBgNVBAMMFklzc3VlIDIwNiBUZXN0IFJvb3QgQ0EwHhcN +MjYwODIyMTAxNjQ4WhcNNDYwODE3MTAxNjQ4WjBlMQswCQYDVQQGEwJJVDEiMCAG +A1UECgwZRmlsZSBWaWV3ZXIgVGVzdCBGaXh0dXJlczEVMBMGA1UECwwMVGVzdGlu +ZyBPbmx5MRswGQYDVQQDDBJJc3N1ZSAyMDYgVGVzdCBUU0EwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDaIBW6ggmyXEQLxj1qUO3VQ0XyN2N/ApwE70VV +5105boitUFGrL8GCtZM9cbJShP8aPb0xbTrhJo0AwKf2FbX05/V+YF75MszkkD36 +5phjEfrElfWUhXs0ZzCGXJVuhiTyjONat81BtTFOD940y2WjYnaUDH/shktlQhOm +o78igiBrN4zgNhtRzFuMMnClOW3rRBI8yyvSlFARQP9M4KodaUY2dJ31daaJo6tA +qITcRU1HomFlarWs/s+kKTqIrFq0JGjo0iayGWMXM1PbhMwC/TKd0ApVcrG5NyMu +hBgbiJ4meenG7CXp7ZnnmxsGstt96QIUnOqkDZZdqCHSCQ7pAgMBAAGjgZMwgZAw +DAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/BAwwCgYIKwYB +BQUHAwgwHQYDVR0OBBYEFIxqhnkOSJiqHQUwT0kq9filiBEXMB8GA1UdIwQYMBaA +FJO8Cw+Yfy2A4Yy/XfRT+e0BqSzPMBgGA1UdIAQRMA8wDQYLKwYBBAGDsgOBThQw +DQYJKoZIhvcNAQELBQADggEBAFc7HdmaBUbYvRKQWdDAETRO1UJCZz830r+DYxm8 +r9u909MCNNqBnpzqOBXIocE6uoTfDzkk+UkiegG1onUou7ZqOJspz5+vBJgrGci6 +YFe7Hdj5GE68bEpNTc7zbJQjBmZvoKpt6j5sUuZbPDgCEMRZIyqJTGmjpWOqQQVE +nQw2hugCyzH1D6xuhRBWUdk7jbIYjkUv2ObsmeX/YwLu2zaCC/2dtYElFykwC6qU +SXMPua9Ucvg3gTXkejMw4rDPG7tXjKtTLSTC9JqmAJiSQIgOxET/t1C5oGJqS8F7 +OyjnV3G9cNRdpuaivlH+T6BQHwibM+V5ZB8lAyDdFaQg4pY= +-----END CERTIFICATE----- diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/cms/certificate-chain-with-crl.p7c b/packages/renderers/signature/test/fixtures/github-206-contributed/cms/certificate-chain-with-crl.p7c new file mode 100644 index 0000000000000000000000000000000000000000..6b1fdf4beffca990827f21347db84a54e41b2bdc GIT binary patch literal 4365 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 literal 0 HcmV?d00001 diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/cms/certificate-only-misnamed.p7m b/packages/renderers/signature/test/fixtures/github-206-contributed/cms/certificate-only-misnamed.p7m new file mode 100644 index 0000000000000000000000000000000000000000..5d88e0084ce2fbcf55fb88f2b00461428e4a7dee GIT binary patch literal 3873 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 literal 0 HcmV?d00001 diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/cms/hello-multi-signer.p7m b/packages/renderers/signature/test/fixtures/github-206-contributed/cms/hello-multi-signer.p7m new file mode 100644 index 0000000000000000000000000000000000000000..ab41ae02809ca91702f61ee81164dd106f00f6d8 GIT binary patch literal 3858 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 L>(&eOmEwN^t7txG literal 0 HcmV?d00001 diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/cms/invoice-encapsulated.cmsc b/packages/renderers/signature/test/fixtures/github-206-contributed/cms/invoice-encapsulated.cmsc new file mode 100644 index 0000000000000000000000000000000000000000..e75a182a4a4caabee60ff694cd1004af60e893fa GIT binary patch literal 4831 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 w`3@Xanvs~%?HOy0MTk2@*C9i(?5Ay6nQ~&?~ literal 0 HcmV?d00001 diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/cms/invoice-encapsulated.pdf.p7m b/packages/renderers/signature/test/fixtures/github-206-contributed/cms/invoice-encapsulated.pdf.p7m new file mode 100644 index 0000000000000000000000000000000000000000..e75a182a4a4caabee60ff694cd1004af60e893fa GIT binary patch literal 4831 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 w`3@Xanvs~%?HOy0MTk2@*C9i(?5Ay6nQ~&?~ literal 0 HcmV?d00001 diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/cms/invoice-encapsulated.pem.pkcs7 b/packages/renderers/signature/test/fixtures/github-206-contributed/cms/invoice-encapsulated.pem.pkcs7 new file mode 100644 index 000000000..69bbe8417 --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/cms/invoice-encapsulated.pem.pkcs7 @@ -0,0 +1,103 @@ +-----BEGIN CMS----- +MIIS2wYJKoZIhvcNAQcCoIISzDCCEsgCAQExDTALBglghkgBZQMEAgEwggjSBgkq +hkiG9w0BBwGgggjDBIIIvyVQREYtMS4zCiWTjIueIFJlcG9ydExhYiBHZW5lcmF0 +ZWQgUERGIGRvY3VtZW50IChvcGVuc291cmNlKQoxIDAgb2JqCjw8Ci9GMSAyIDAg +UiAvRjIgMyAwIFIgL0YzIDQgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9CYXNl +Rm9udCAvSGVsdmV0aWNhIC9FbmNvZGluZyAvV2luQW5zaUVuY29kaW5nIC9OYW1l +IC9GMSAvU3VidHlwZSAvVHlwZTEgL1R5cGUgL0ZvbnQKPj4KZW5kb2JqCjMgMCBv +YmoKPDwKL0Jhc2VGb250IC9IZWx2ZXRpY2EtQm9sZCAvRW5jb2RpbmcgL1dpbkFu +c2lFbmNvZGluZyAvTmFtZSAvRjIgL1N1YnR5cGUgL1R5cGUxIC9UeXBlIC9Gb250 +Cj4+CmVuZG9iago0IDAgb2JqCjw8Ci9CYXNlRm9udCAvSGVsdmV0aWNhLU9ibGlx +dWUgL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcgL05hbWUgL0YzIC9TdWJ0eXBl +IC9UeXBlMSAvVHlwZSAvRm9udAo+PgplbmRvYmoKNSAwIG9iago8PAovQ29udGVu +dHMgOSAwIFIgL01lZGlhQm94IFsgMCAwIDU5NS4yNzU2IDg0MS44ODk4IF0gL1Bh +cmVudCA4IDAgUiAvUmVzb3VyY2VzIDw8Ci9Gb250IDEgMCBSIC9Qcm9jU2V0IFsg +L1BERiAvVGV4dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSSBdCj4+IC9Sb3RhdGUg +MCAvVHJhbnMgPDwKCj4+IAogIC9UeXBlIC9QYWdlCj4+CmVuZG9iago2IDAgb2Jq +Cjw8Ci9QYWdlTW9kZSAvVXNlTm9uZSAvUGFnZXMgOCAwIFIgL1R5cGUgL0NhdGFs +b2cKPj4KZW5kb2JqCjcgMCBvYmoKPDwKL0F1dGhvciAoT3BlbkFJIC0gc3ludGhl +dGljIHRlc3QgZml4dHVyZSkgL0NyZWF0aW9uRGF0ZSAoRDoyMDI2MDgyMjEwMTY0 +OCswMCcwMCcpIC9DcmVhdG9yIChhbm9ueW1vdXMpIC9LZXl3b3JkcyAoKSAvTW9k +RGF0ZSAoRDoyMDI2MDgyMjEwMTY0OCswMCcwMCcpIC9Qcm9kdWNlciAoUmVwb3J0 +TGFiIFBERiBMaWJyYXJ5IC0gXChvcGVuc291cmNlXCkpIAogIC9TdWJqZWN0IChO +b24tcHJvZHVjdGlvbiBjcnlwdG9ncmFwaGljIGZpeHR1cmUpIC9UaXRsZSAoU3lu +dGhldGljIGludm9pY2UgZml4dHVyZSBmb3IgZmlsZS12aWV3ZXIgaXNzdWUgMjA2 +KSAvVHJhcHBlZCAvRmFsc2UKPj4KZW5kb2JqCjggMCBvYmoKPDwKL0NvdW50IDEg +L0tpZHMgWyA1IDAgUiBdIC9UeXBlIC9QYWdlcwo+PgplbmRvYmoKOSAwIG9iago8 +PAovRmlsdGVyIFsgL0FTQ0lJODVEZWNvZGUgL0ZsYXRlRGVjb2RlIF0gL0xlbmd0 +aCA2MDkKPj4Kc3RyZWFtCkdhdD1oYyZVNywnTHFpZydSJ2pTMHQtXSJQIllxPjpf +Q1dFTTU/S0tYXGlPdE5gMkNVYSVcWFdjPS1vV1VyOWpNYzk4YDtBLTFDSEE5a1hJ +XmRlVXQuZnRBKmdCMzovIlJITVxsc1prbCtQRVNkI3RrM0phVU9BLydJSyYyUk0o +OWZrXVpVZlZZUksqVDFgMyRecmhCdF4iSWs6TXQvSShUPjk9XiFQPSkyQmgyVUk3 +UFRlQjdETj1eRC4pOjYnRikpJF8tazJRTUkwYzFCTGdGTFJpcWQuVGJAW2ZKX1Jm +J0EqWy9DaEcqIUBGOD9FbFBNRC1BYk5SQmU8W1RKJyFWUEsuczdtN2lgW0hoUEhX +SU5gbERBaFYiLGA5QkBuYVElZ3IpIThZJmBQXiQwKkRBX05cXWkmIlM1Q1QnXiVr +J1k1QnJSX0EnXiowRHIhKE4oc2U8cWo9W0NKbElMaFVcNy9iNWNWSCVhPzkkaHIy +dWQ3PCtKQ2ZMSVspN0xpSTZqbGEuLidrU01nNHI/SSdBSUdWQFE6cD1lVENdWS86 +PWksV0xGPm1nR2o0ZzppbFhrVHFcY1MvP01UTDhKZEE3JDY9QU88YzpkNC42WVBd +R2tbYkU/LiYmSDBqaW5YOT4vLzI3T2lqVGo/OGdjLUZbOWlOJ24uPlc4OWw3RUNX +LWQwRFZMcGMjQyFyKUsyZkwxOEljbk5eX2dYZEwmXyJfVEtgYCE6ND9WTTlcamlV +ImRDMz03bGVGXVtvNHJFSTFHZjdRNXNVTE8yQUpJbiRxSW1BSDM3VSFGPS0raCN+ +PmVuZHN0cmVhbQplbmRvYmoKeHJlZgowIDEwCjAwMDAwMDAwMDAgNjU1MzUgZiAK +MDAwMDAwMDA2MSAwMDAwMCBuIAowMDAwMDAwMTEyIDAwMDAwIG4gCjAwMDAwMDAy +MTkgMDAwMDAgbiAKMDAwMDAwMDMzMSAwMDAwMCBuIAowMDAwMDAwNDQ2IDAwMDAw +IG4gCjAwMDAwMDA2NDkgMDAwMDAgbiAKMDAwMDAwMDcxNyAwMDAwMCBuIAowMDAw +MDAxMDY4IDAwMDAwIG4gCjAwMDAwMDExMjcgMDAwMDAgbiAKdHJhaWxlcgo8PAov +SUQgCls8MWYyNGE2MDljNmI0YWFlNjVhNTk4MzA1Yzg3ZjQwMDA+PDFmMjRhNjA5 +YzZiNGFhZTY1YTU5ODMwNWM4N2Y0MDAwPl0KJSBSZXBvcnRMYWIgZ2VuZXJhdGVk +IFBERiBkb2N1bWVudCAtLSBkaWdlc3QgKG9wZW5zb3VyY2UpCgovSW5mbyA3IDAg +UgovUm9vdCA2IDAgUgovU2l6ZSAxMAo+PgpzdGFydHhyZWYKMTgyNgolJUVPRgqg +ggfUMIIDxjCCAq6gAwIBAgIUM3G7+wOL77qYdrnvvB2ZBZqvKuUwDQYJKoZIhvcN +AQELBQAwaTELMAkGA1UEBhMCSVQxIjAgBgNVBAoMGUZpbGUgVmlld2VyIFRlc3Qg +Rml4dHVyZXMxFTATBgNVBAsMDFRlc3RpbmcgT25seTEfMB0GA1UEAwwWSXNzdWUg +MjA2IFRlc3QgUm9vdCBDQTAeFw0yNjA4MjIxMDE2NDhaFw00NjA4MjIxMDE2NDha +MGkxCzAJBgNVBAYTAklUMSIwIAYDVQQKDBlGaWxlIFZpZXdlciBUZXN0IEZpeHR1 +cmVzMRUwEwYDVQQLDAxUZXN0aW5nIE9ubHkxHzAdBgNVBAMMFklzc3VlIDIwNiBU +ZXN0IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvS14M +jtjlXeEiYVQUH84zNPzoPTjBc6yyZc/pvRbJmyMcBzsIXRk8vNZSuVK2365a49aM +noyCYn2VZHk/6PW/IysWGNs9ubonMQuE0gGXcH2BWGdy9kfpyxlT5GlJpOP5B6dV +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 +-----END CMS----- diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/cms/xml-encapsulated-but-named.p7s b/packages/renderers/signature/test/fixtures/github-206-contributed/cms/xml-encapsulated-but-named.p7s new file mode 100644 index 0000000000000000000000000000000000000000..9bed758c17e86728c1bb80f694d50a12d2dcec4a GIT binary patch literal 2713 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 literal 0 HcmV?d00001 diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/originals/sample.xml b/packages/renderers/signature/test/fixtures/github-206-contributed/originals/sample.xml new file mode 100644 index 000000000..d09e78032 --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/originals/sample.xml @@ -0,0 +1,2 @@ + +Synthetic XML content diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/reports/openssl-inspection.txt b/packages/renderers/signature/test/fixtures/github-206-contributed/reports/openssl-inspection.txt new file mode 100644 index 000000000..24be41d6c --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/reports/openssl-inspection.txt @@ -0,0 +1,1481 @@ +=== CMS invoice encapsulated === +CMS_ContentInfo: + contentType: pkcs7-signedData (1.2.840.113549.1.7.2) + d.signedData: + version: 1 + digestAlgorithms: + algorithm: sha256 (2.16.840.1.101.3.4.2.1) + parameter: + encapContentInfo: + eContentType: pkcs7-data (1.2.840.113549.1.7.1) + eContent: + 0000 - 25 50 44 46 2d 31 2e 33-0a 25 93 8c 8b 9e 20 %PDF-1.3.%.... + 000f - 52 65 70 6f 72 74 4c 61-62 20 47 65 6e 65 72 ReportLab Gener + 001e - 61 74 65 64 20 50 44 46-20 64 6f 63 75 6d 65 ated PDF docume + 002d - 6e 74 20 28 6f 70 65 6e-73 6f 75 72 63 65 29 nt (opensource) + 003c - 0a 31 20 30 20 6f 62 6a-0a 3c 3c 0a 2f 46 31 .1 0 obj.<<./F1 + 004b - 20 32 20 30 20 52 20 2f-46 32 20 33 20 30 20 2 0 R /F2 3 0 + 005a - 52 20 2f 46 33 20 34 20-30 20 52 0a 3e 3e 0a R /F3 4 0 R.>>. + 0069 - 65 6e 64 6f 62 6a 0a 32-20 30 20 6f 62 6a 0a endobj.2 0 obj. + 0078 - 3c 3c 0a 2f 42 61 73 65-46 6f 6e 74 20 2f 48 <<./BaseFont /H + 0087 - 65 6c 76 65 74 69 63 61-20 2f 45 6e 63 6f 64 elvetica /Encod + 0096 - 69 6e 67 20 2f 57 69 6e-41 6e 73 69 45 6e 63 ing /WinAnsiEnc + 00a5 - 6f 64 69 6e 67 20 2f 4e-61 6d 65 20 2f 46 31 oding /Name /F1 + 00b4 - 20 2f 53 75 62 74 79 70-65 20 2f 54 79 70 65 /Subtype /Type + 00c3 - 31 20 2f 54 79 70 65 20-2f 46 6f 6e 74 0a 3e 1 /Type /Font.> + 00d2 - 3e 0a 65 6e 64 6f 62 6a-0a 33 20 30 20 6f 62 >.endobj.3 0 ob + 00e1 - 6a 0a 3c 3c 0a 2f 42 61-73 65 46 6f 6e 74 20 j.<<./BaseFont + 00f0 - 2f 48 65 6c 76 65 74 69-63 61 2d 42 6f 6c 64 /Helvetica-Bold + 00ff - 20 2f 45 6e 63 6f 64 69-6e 67 20 2f 57 69 6e /Encoding /Win + 010e - 41 6e 73 69 45 6e 63 6f-64 69 6e 67 20 2f 4e AnsiEncoding /N + 011d - 61 6d 65 20 2f 46 32 20-2f 53 75 62 74 79 70 ame /F2 /Subtyp + 012c - 65 20 2f 54 79 70 65 31-20 2f 54 79 70 65 20 e /Type1 /Type + 013b - 2f 46 6f 6e 74 0a 3e 3e-0a 65 6e 64 6f 62 6a /Font.>>.endobj + 014a - 0a 34 20 30 20 6f 62 6a-0a 3c 3c 0a 2f 42 61 .4 0 obj.<<./Ba + 0159 - 73 65 46 6f 6e 74 20 2f-48 65 6c 76 65 74 69 seFont /Helveti + 0168 - 63 61 2d 4f 62 6c 69 71-75 65 20 2f 45 6e 63 ca-Oblique /Enc + 0177 - 6f 64 69 6e 67 20 2f 57-69 6e 41 6e 73 69 45 oding /WinAnsiE + 0186 - 6e 63 6f 64 69 6e 67 20-2f 4e 61 6d 65 20 2f ncoding /Name / + 0195 - 46 33 20 2f 53 75 62 74-79 70 65 20 2f 54 79 F3 /Subtype /Ty + 01a4 - 70 65 31 20 2f 54 79 70-65 20 2f 46 6f 6e 74 pe1 /Type /Font + 01b3 - 0a 3e 3e 0a 65 6e 64 6f-62 6a 0a 35 20 30 20 .>>.endobj.5 0 + 01c2 - 6f 62 6a 0a 3c 3c 0a 2f-43 6f 6e 74 65 6e 74 obj.<<./Content + 01d1 - 73 20 39 20 30 20 52 20-2f 4d 65 64 69 61 42 s 9 0 R /MediaB + 01e0 - 6f 78 20 5b 20 30 20 30-20 35 39 35 2e 32 37 ox [ 0 0 595.27 + 01ef - 35 36 20 38 34 31 2e 38-38 39 38 20 5d 20 2f 56 841.8898 ] / + 01fe - 50 61 72 65 6e 74 20 38-20 30 20 52 20 2f 52 Parent 8 0 R /R + 020d - 65 73 6f 75 72 63 65 73-20 3c 3c 0a 2f 46 6f esources <<./Fo + 021c - 6e 74 20 31 20 30 20 52-20 2f 50 72 6f 63 53 nt 1 0 R /ProcS + 022b - 65 74 20 5b 20 2f 50 44-46 20 2f 54 65 78 74 et [ /PDF /Text + 023a - 20 2f 49 6d 61 67 65 42-20 2f 49 6d 61 67 65 /ImageB /Image + 0249 - 43 20 2f 49 6d 61 67 65-49 20 5d 0a 3e 3e 20 C /ImageI ].>> + 0258 - 2f 52 6f 74 61 74 65 20-30 20 2f 54 72 61 6e /Rotate 0 /Tran + 0267 - 73 20 3c 3c 0a 0a 3e 3e-20 0a 20 20 2f 54 79 s <<..>> . /Ty + 0276 - 70 65 20 2f 50 61 67 65-0a 3e 3e 0a 65 6e 64 pe /Page.>>.end + 0285 - 6f 62 6a 0a 36 20 30 20-6f 62 6a 0a 3c 3c 0a obj.6 0 obj.<<. + 0294 - 2f 50 61 67 65 4d 6f 64-65 20 2f 55 73 65 4e /PageMode /UseN + 02a3 - 6f 6e 65 20 2f 50 61 67-65 73 20 38 20 30 20 one /Pages 8 0 + 02b2 - 52 20 2f 54 79 70 65 20-2f 43 61 74 61 6c 6f R /Type /Catalo + 02c1 - 67 0a 3e 3e 0a 65 6e 64-6f 62 6a 0a 37 20 30 g.>>.endobj.7 0 + 02d0 - 20 6f 62 6a 0a 3c 3c 0a-2f 41 75 74 68 6f 72 obj.<<./Author + 02df - 20 28 4f 70 65 6e 41 49-20 2d 20 73 79 6e 74 (OpenAI - synt + 02ee - 68 65 74 69 63 20 74 65-73 74 20 66 69 78 74 hetic test fixt + 02fd - 75 72 65 29 20 2f 43 72-65 61 74 69 6f 6e 44 ure) /CreationD + 030c - 61 74 65 20 28 44 3a 32-30 32 36 30 38 32 32 ate (D:20260822 + 031b - 31 30 31 36 34 38 2b 30-30 27 30 30 27 29 20 101648+00'00') + 032a - 2f 43 72 65 61 74 6f 72-20 28 61 6e 6f 6e 79 /Creator (anony + 0339 - 6d 6f 75 73 29 20 2f 4b-65 79 77 6f 72 64 73 mous) /Keywords + 0348 - 20 28 29 20 2f 4d 6f 64-44 61 74 65 20 28 44 () /ModDate (D + 0357 - 3a 32 30 32 36 30 38 32-32 31 30 31 36 34 38 :20260822101648 + 0366 - 2b 30 30 27 30 30 27 29-20 2f 50 72 6f 64 75 +00'00') /Produ + 0375 - 63 65 72 20 28 52 65 70-6f 72 74 4c 61 62 20 cer (ReportLab + 0384 - 50 44 46 20 4c 69 62 72-61 72 79 20 2d 20 5c PDF Library - \ + 0393 - 28 6f 70 65 6e 73 6f 75-72 63 65 5c 29 29 20 (opensource\)) + 03a2 - 0a 20 20 2f 53 75 62 6a-65 63 74 20 28 4e 6f . /Subject (No + 03b1 - 6e 2d 70 72 6f 64 75 63-74 69 6f 6e 20 63 72 n-production cr + 03c0 - 79 70 74 6f 67 72 61 70-68 69 63 20 66 69 78 yptographic fix + 03cf - 74 75 72 65 29 20 2f 54-69 74 6c 65 20 28 53 ture) /Title (S + 03de - 79 6e 74 68 65 74 69 63-20 69 6e 76 6f 69 63 ynthetic invoic + 03ed - 65 20 66 69 78 74 75 72-65 20 66 6f 72 20 66 e fixture for f + 03fc - 69 6c 65 2d 76 69 65 77-65 72 20 69 73 73 75 ile-viewer issu + 040b - 65 20 32 30 36 29 20 2f-54 72 61 70 70 65 64 e 206) /Trapped + 041a - 20 2f 46 61 6c 73 65 0a-3e 3e 0a 65 6e 64 6f /False.>>.endo + 0429 - 62 6a 0a 38 20 30 20 6f-62 6a 0a 3c 3c 0a 2f bj.8 0 obj.<<./ + 0438 - 43 6f 75 6e 74 20 31 20-2f 4b 69 64 73 20 5b Count 1 /Kids [ + 0447 - 20 35 20 30 20 52 20 5d-20 2f 54 79 70 65 20 5 0 R ] /Type + 0456 - 2f 50 61 67 65 73 0a 3e-3e 0a 65 6e 64 6f 62 /Pages.>>.endob + 0465 - 6a 0a 39 20 30 20 6f 62-6a 0a 3c 3c 0a 2f 46 j.9 0 obj.<<./F + 0474 - 69 6c 74 65 72 20 5b 20-2f 41 53 43 49 49 38 ilter [ /ASCII8 + 0483 - 35 44 65 63 6f 64 65 20-2f 46 6c 61 74 65 44 5Decode /FlateD + 0492 - 65 63 6f 64 65 20 5d 20-2f 4c 65 6e 67 74 68 ecode ] /Length + 04a1 - 20 36 30 39 0a 3e 3e 0a-73 74 72 65 61 6d 0a 609.>>.stream. + 04b0 - 47 61 74 3d 68 63 26 55-37 2c 27 4c 71 69 67 Gat=hc&U7,'Lqig + 04bf - 27 52 27 6a 53 30 74 2d-5d 22 50 22 59 71 3e 'R'jS0t-]"P"Yq> + 04ce - 3a 5f 43 57 45 4d 35 3f-4b 4b 58 5c 69 4f 74 :_CWEM5?KKX\iOt + 04dd - 4e 60 32 43 55 61 25 5c-58 57 63 3d 2d 6f 57 N`2CUa%\XWc=-oW + 04ec - 55 72 39 6a 4d 63 39 38-60 3b 41 2d 31 43 48 Ur9jMc98`;A-1CH + 04fb - 41 39 6b 58 49 5e 64 65-55 74 2e 66 74 41 2a A9kXI^deUt.ftA* + 050a - 67 42 33 3a 2f 22 52 48-4d 5c 6c 73 5a 6b 6c gB3:/"RHM\lsZkl + 0519 - 2b 50 45 53 64 23 74 6b-33 4a 61 55 4f 41 2f +PESd#tk3JaUOA/ + 0528 - 27 49 4b 26 32 52 4d 28-39 66 6b 5d 5a 55 66 'IK&2RM(9fk]ZUf + 0537 - 56 59 52 4b 2a 54 31 60-33 24 5e 72 68 42 74 VYRK*T1`3$^rhBt + 0546 - 5e 22 49 6b 3a 4d 74 2f-49 28 54 3e 39 3d 5e ^"Ik:Mt/I(T>9=^ + 0555 - 21 50 3d 29 32 42 68 32-55 49 37 50 54 65 42 !P=)2Bh2UI7PTeB + 0564 - 37 44 4e 3d 5e 44 2e 29-3a 36 27 46 29 29 24 7DN=^D.):6'F))$ + 0573 - 5f 2d 6b 32 51 4d 49 30-63 31 42 4c 67 46 4c _-k2QMI0c1BLgFL + 0582 - 52 69 71 64 2e 54 62 40-5b 66 4a 5f 52 66 27 Riqd.Tb@[fJ_Rf' + 0591 - 41 2a 5b 2f 43 68 47 2a-21 40 46 38 3f 45 6c A*[/ChG*!@F8?El + 05a0 - 50 4d 44 2d 41 62 4e 52-42 65 3c 5b 54 4a 27 PMD-AbNRBe<[TJ' + 05af - 21 56 50 4b 2e 73 37 6d-37 69 60 5b 48 68 50 !VPK.s7m7i`[HhP + 05be - 48 57 49 4e 60 6c 44 41-68 56 22 2c 60 39 42 HWIN`lDAhV",`9B + 05cd - 40 6e 61 51 25 67 72 29-21 38 59 26 60 50 5e @naQ%gr)!8Y&`P^ + 05dc - 24 30 2a 44 41 5f 4e 5c-5d 69 26 22 53 35 43 $0*DA_N\]i&"S5C + 05eb - 54 27 5e 25 6b 27 59 35-42 72 52 5f 41 27 5e T'^%k'Y5BrR_A'^ + 05fa - 2a 30 44 72 21 28 4e 28-73 65 3c 71 6a 3d 5b *0Dr!(N(semgGj4g:ilXkT + 0663 - 71 5c 63 53 2f 3f 4d 54-4c 38 4a 64 41 37 24 q\cS/?MTL8JdA7$ + 0672 - 36 3d 41 4f 3c 63 3a 64-34 2e 36 59 50 5d 47 6=AO//27OijTj?8gc- + 069f - 46 5b 39 69 4e 27 6e 2e-3e 57 38 39 6c 37 45 F[9iN'n.>W89l7E + 06ae - 43 57 2d 64 30 44 56 4c-70 63 23 43 21 72 29 CW-d0DVLpc#C!r) + 06bd - 4b 32 66 4c 31 38 49 63-6e 4e 5e 5f 67 58 64 K2fL18IcnN^_gXd + 06cc - 4c 26 5f 22 5f 54 4b 60-60 21 3a 34 3f 56 4d L&_"_TK``!:4?VM + 06db - 39 5c 6a 69 55 22 64 43-33 3d 37 6c 65 46 5d 9\jiU"dC3=7leF] + 06ea - 5b 6f 34 72 45 49 31 47-66 37 51 35 73 55 4c [o4rEI1Gf7Q5sUL + 06f9 - 4f 32 41 4a 49 6e 24 71-49 6d 41 48 33 37 55 O2AJIn$qImAH37U + 0708 - 21 46 3d 2d 2b 68 23 7e-3e 65 6e 64 73 74 72 !F=-+h#~>endstr + 0717 - 65 61 6d 0a 65 6e 64 6f-62 6a 0a 78 72 65 66 eam.endobj.xref + 0726 - 0a 30 20 31 30 0a 30 30-30 30 30 30 30 30 30 .0 10.000000000 + 0735 - 30 20 36 35 35 33 35 20-66 20 0a 30 30 30 30 0 65535 f .0000 + 0744 - 30 30 30 30 36 31 20 30-30 30 30 30 20 6e 20 000061 00000 n + 0753 - 0a 30 30 30 30 30 30 30-31 31 32 20 30 30 30 .0000000112 000 + 0762 - 30 30 20 6e 20 0a 30 30-30 30 30 30 30 32 31 00 n .000000021 + 0771 - 39 20 30 30 30 30 30 20-6e 20 0a 30 30 30 30 9 00000 n .0000 + 0780 - 30 30 30 33 33 31 20 30-30 30 30 30 20 6e 20 000331 00000 n + 078f - 0a 30 30 30 30 30 30 30-34 34 36 20 30 30 30 .0000000446 000 + 079e - 30 30 20 6e 20 0a 30 30-30 30 30 30 30 36 34 00 n .000000064 + 07ad - 39 20 30 30 30 30 30 20-6e 20 0a 30 30 30 30 9 00000 n .0000 + 07bc - 30 30 30 37 31 37 20 30-30 30 30 30 20 6e 20 000717 00000 n + 07cb - 0a 30 30 30 30 30 30 31-30 36 38 20 30 30 30 .0000001068 000 + 07da - 30 30 20 6e 20 0a 30 30-30 30 30 30 31 31 32 00 n .000000112 + 07e9 - 37 20 30 30 30 30 30 20-6e 20 0a 74 72 61 69 7 00000 n .trai + 07f8 - 6c 65 72 0a 3c 3c 0a 2f-49 44 20 0a 5b 3c 31 ler.<<./ID .[<1 + 0807 - 66 32 34 61 36 30 39 63-36 62 34 61 61 65 36 f24a609c6b4aae6 + 0816 - 35 61 35 39 38 33 30 35-63 38 37 66 34 30 30 5a598305c87f400 + 0825 - 30 3e 3c 31 66 32 34 61-36 30 39 63 36 62 34 0><1f24a609c6b4 + 0834 - 61 61 65 36 35 61 35 39-38 33 30 35 63 38 37 aae65a598305c87 + 0843 - 66 34 30 30 30 3e 5d 0a-25 20 52 65 70 6f 72 f4000>].% Repor + 0852 - 74 4c 61 62 20 67 65 6e-65 72 61 74 65 64 20 tLab generated + 0861 - 50 44 46 20 64 6f 63 75-6d 65 6e 74 20 2d 2d PDF document -- + 0870 - 20 64 69 67 65 73 74 20-28 6f 70 65 6e 73 6f digest (openso + 087f - 75 72 63 65 29 0a 0a 2f-49 6e 66 6f 20 37 20 urce)../Info 7 + 088e - 30 20 52 0a 2f 52 6f 6f-74 20 36 20 30 20 52 0 R./Root 6 0 R + 089d - 0a 2f 53 69 7a 65 20 31-30 0a 3e 3e 0a 73 74 ./Size 10.>>.st + 08ac - 61 72 74 78 72 65 66 0a-31 38 32 36 0a 25 25 artxref.1826.%% + 08bb - 45 4f 46 0a EOF. + certificates: + d.certificate: + cert_info: + version: 2 + serialNumber: 0x3371BBFB038BEFBA9876B9EFBC1D99059AAF2AE5 + signature: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + validity: + notBefore: Aug 22 10:16:48 2026 GMT + notAfter: Aug 22 10:16:48 2046 GMT + subject: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + key: X509_PUBKEY: + algor: + algorithm: rsaEncryption (1.2.840.113549.1.1.1) + parameter: NULL + public_key: (0 unused bits) + 0000 - 30 82 01 0a 02 82 01 01-00 af 4b 5e 0c 8e 0.........K^.. + 000e - d8 e5 5d e1 22 61 54 14-1f ce 33 34 fc e8 ..]."aT...34.. + 001c - 3d 38 c1 73 ac b2 65 cf-e9 bd 16 c9 9b 23 =8.s..e......# + 002a - 1c 07 3b 08 5d 19 3c bc-d6 52 b9 52 b6 df ..;.].<..R.R.. + 0038 - ae 5a e3 d6 8c 9e 8c 82-62 7d 95 64 79 3f .Z......b}.dy? + 0046 - e8 f5 bf 23 2b 16 18 db-3d b9 ba 27 31 0b ...#+...=..'1. + 0054 - 84 d2 01 97 70 7d 81 58-67 72 f6 47 e9 cb ....p}.Xgr.G.. + 0062 - 19 53 e4 69 49 a4 e3 f9-07 a7 55 be c3 41 .S.iI.....U..A + 0070 - c0 b5 13 03 c7 ac 3d 2f-c4 89 0e 54 76 73 ......=/...Tvs + 007e - fd 4a 89 c3 29 f0 70 17-4d 83 7b 6f b7 10 .J..).p.M.{o.. + 008c - 10 df 3e 16 fd db 73 8f-9b 46 4f 8b d9 30 ..>...s..FO..0 + 009a - ed 98 9b 3b da 22 33 d4-e4 37 fc e9 ee 0d ...;."3..7.... + 00a8 - a3 2b 0a 33 5d 2e e8 8c-b6 97 46 5a 53 0b .+.3].....FZS. + 00b6 - ed 61 ff 35 84 64 74 c3-59 70 f0 4b a5 3f .a.5.dt.Yp.K.? + 00c4 - b0 eb 31 41 8f d9 ef 02-55 c8 ea 31 f7 bc ..1A....U..1.. + 00d2 - a0 c8 25 f1 fd 41 0a c6-e3 ff 19 58 80 cc ..%..A.....X.. + 00e0 - ba 27 5c ff b3 41 c4 67-33 60 e5 2a 7b 41 .'\..A.g3`.*{A + 00ee - 59 70 88 1b 34 f4 e6 68-d4 16 6b 42 e6 72 Yp..4..h..kB.r + 00fc - 7c af a6 4b 20 0d 9d 59-8f 6b e6 c2 db 02 |..K ..Y.k.... + 010a - 03 01 00 01 .... + issuerUID: + subjectUID: + extensions: + object: X509v3 Authority Key Identifier (2.5.29.35) + critical: FALSE + value: + 0000 - 30 16 80 14 93 bc 0b 0f-98 7f 2d 80 e1 0.........-.. + 000d - 8c bf 5d f4 53 f9 ed 01-a9 2c cf ..].S....,. + + object: X509v3 Basic Constraints (2.5.29.19) + critical: TRUE + value: + 0000 - 30 06 01 01 ff 02 01 01- 0....... + + object: X509v3 Key Usage (2.5.29.15) + critical: TRUE + value: + 0000 - 03 02 01 06 .... + + object: X509v3 Subject Key Identifier (2.5.29.14) + critical: FALSE + value: + 0000 - 04 14 93 bc 0b 0f 98 7f-2d 80 e1 8c bf ........-.... + 000d - 5d f4 53 f9 ed 01 a9 2c-cf ].S....,. + sig_alg: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + signature: (0 unused bits) + 0000 - 5d 0b aa 6b 92 25 b9 c6-bd 4c 06 20 11 12 75 ]..k.%...L. ..u + 000f - 65 f9 21 b4 f4 21 06 c4-26 26 c2 3e 37 ca cc e.!..!..&&.>7.. + 001e - 2e 6d 83 b2 15 b8 9b c2-47 d9 a0 47 7a c4 a9 .m......G..Gz.. + 002d - a7 90 0c 30 2a 0c 70 c5-12 c2 a6 fa 01 74 b5 ...0*.p......t. + 003c - 91 e4 65 9b b7 a6 8e 93-db 6a bb 6b 0c bb f5 ..e......j.k... + 004b - ef 83 3a eb ba 24 9a 8f-b6 f1 82 f8 c7 41 2c ..:..$.......A, + 005a - 3d 99 14 96 ae 82 83 be-29 2b 87 5d 96 66 fd =.......)+.].f. + 0069 - e5 e9 09 70 29 eb 2e a1-01 04 42 7f 85 81 db ...p).....B.... + 0078 - cc 18 8f b6 a9 64 3a dc-3e 97 93 fe 94 36 90 .....d:.>....6. + 0087 - 21 8e 27 b6 c7 21 eb f9-0f c4 cf 04 79 84 a2 !.'..!......y.. + 0096 - 11 16 8b 16 9c ae af 20-c1 0b 08 de f6 56 84 ....... .....V. + 00a5 - 5d 61 44 fe 38 1c 24 f4-96 8e ac 19 93 6c 4f ]aD.8.$......lO + 00b4 - a2 c8 d0 a6 6e 3e a0 1e-9f f8 1c e7 74 d8 5b ....n>......t.[ + 00c3 - a9 55 51 43 25 74 17 77-5a 56 97 b2 09 84 8f .UQC%t.wZV..... + 00d2 - f4 ed 7b b4 47 6c f7 87-c4 da 3f 5d 16 f3 e8 ..{.Gl....?]... + 00e1 - 07 28 7d ea 09 93 d2 0d-43 af 8a 9c e6 b9 da .(}.....C...... + 00f0 - 6d 76 98 12 b5 8a 31 6b-2c 84 f6 79 20 4a 08 mv....1k,..y J. + 00ff - 96 . + + d.certificate: + cert_info: + version: 2 + serialNumber: 132769 + signature: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + validity: + notBefore: Aug 22 10:16:48 2026 GMT + notAfter: Aug 17 10:16:48 2046 GMT + subject: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=CMS Test Signer Alice, emailAddress=alice@example.invalid + key: X509_PUBKEY: + algor: + algorithm: rsaEncryption (1.2.840.113549.1.1.1) + parameter: NULL + public_key: (0 unused bits) + 0000 - 30 82 01 0a 02 82 01 01-00 c6 32 7b 59 a4 0.........2{Y. + 000e - 98 14 ff 12 83 e6 e7 49-df b3 b5 e6 ee 2b .......I.....+ + 001c - c1 7b 33 31 67 3d e1 f4-c9 52 7b b5 6f 6e .{31g=...R{.on + 002a - 40 a9 7b 34 ea 75 ea 0b-04 a2 81 c5 20 7e @.{4.u...... ~ + 0038 - 6b 95 61 cf d7 01 38 05-5a 29 d7 38 30 47 k.a...8.Z).80G + 0046 - 6e ca 2b af d2 5a 25 fd-d1 96 5a 8a 34 0e n.+..Z%...Z.4. + 0054 - e2 61 35 de 72 2c e4 8c-5e c9 7d da 46 86 .a5.r,..^.}.F. + 0062 - 94 9f 8a f6 07 02 61 cf-e4 59 8c 53 10 5a ......a..Y.S.Z + 0070 - a2 33 b6 05 06 27 94 54-1a a8 0f 9b 80 a1 .3...'.T...... + 007e - 43 94 a2 56 a9 fc 92 ba-42 48 bb a3 df db C..V....BH.... + 008c - 14 10 cc c0 ef 40 21 65-0e c4 0c e3 b6 f7 .....@!e...... + 009a - 53 4b d0 fe 4e 24 26 e1-a7 8c 7c 04 39 64 SK..N$&...|.9d + 00a8 - 52 7b 14 34 29 aa 80 f2-91 58 2f f2 81 b5 R{.4)....X/... + 00b6 - 46 23 f2 e6 d4 67 65 6e-35 56 d3 17 07 21 F#...gen5V...! + 00c4 - 60 aa f2 21 e0 8f ac 9b-2c 67 36 1a e3 72 `..!....,g6..r + 00d2 - c3 f8 05 e1 9a 28 af 0e-f1 cd 47 a8 88 1f .....(....G... + 00e0 - 6e 02 53 ee fd 1a b9 e4-d3 e3 df cb 3c f5 n.S.........<. + 00ee - 61 c7 8b e9 f2 28 f3 26-ed 10 59 d2 4a 26 a....(.&..Y.J& + 00fc - 95 ce 34 52 57 ac f7 50-ba 4f f8 45 b9 02 ..4RW..P.O.E.. + 010a - 03 01 00 01 .... + issuerUID: + subjectUID: + extensions: + object: X509v3 Basic Constraints (2.5.29.19) + critical: TRUE + value: + 0000 - 30 00 0. + + object: X509v3 Key Usage (2.5.29.15) + critical: TRUE + value: + 0000 - 03 02 07 80 .... + + object: X509v3 Extended Key Usage (2.5.29.37) + critical: FALSE + value: + 0000 - 30 0a 06 08 2b 06 01 05-05 07 03 04 0...+....... + + object: X509v3 Subject Key Identifier (2.5.29.14) + critical: FALSE + value: + 0000 - 04 14 d6 93 b0 2f ea ed-46 ed aa 1c 9b ...../..F.... + 000d - 46 eb b9 e0 a1 56 6f 26-61 F....Vo&a + + object: X509v3 Authority Key Identifier (2.5.29.35) + critical: FALSE + value: + 0000 - 30 16 80 14 93 bc 0b 0f-98 7f 2d 80 e1 0.........-.. + 000d - 8c bf 5d f4 53 f9 ed 01-a9 2c cf ..].S....,. + + object: X509v3 Certificate Policies (2.5.29.32) + critical: FALSE + value: + 0000 - 30 0f 30 0d 06 0b 2b 06-01 04 01 83 b2 0.0...+...... + 000d - 03 81 4e 0a ..N. + sig_alg: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + signature: (0 unused bits) + 0000 - 66 7f 9e 84 1d bf f2 01-38 a9 52 77 6e e9 15 f.......8.Rwn.. + 000f - 53 fd 12 cd 60 13 6d cc-01 c6 25 e1 88 c5 48 S...`.m...%...H + 001e - 7a 31 5d bb 81 88 98 e9-10 74 71 48 06 63 d3 z1]......tqH.c. + 002d - 14 00 21 d3 c1 db 49 20-aa 87 f8 71 6c a0 9e ..!...I ...ql.. + 003c - 34 23 d7 fa 25 d9 b2 75-6c ce 29 b4 6e 88 ca 4#..%..ul.).n.. + 004b - 26 2b d2 0e 36 df 7e ba-58 11 73 49 83 fa 8d &+..6.~.X.sI... + 005a - ac 31 6e d2 ff 84 ab 4b-72 36 0b e0 8f 2a 13 .1n....Kr6...*. + 0069 - 3f 0c b6 a5 a0 4e 39 a8-d6 11 da 97 e3 bd 79 ?....N9.......y + 0078 - 0e 1d 31 37 43 7f 90 6b-c3 8e 9b 65 b6 77 bb ..17C..k...e.w. + 0087 - 12 3c 09 f5 28 c1 b7 2b-ae 50 70 04 2d c4 81 .<..(..+.Pp.-.. + 0096 - 26 34 cc 57 b1 90 e1 6b-f8 f3 5c 16 eb ba 82 &4.W...k..\.... + 00a5 - 13 e4 0a 0b 7c 06 ab 65-d6 5e e1 50 2c 3c c3 ....|..e.^.P,<. + 00b4 - c2 e7 08 97 82 5f 9f 56-80 0f 77 b2 9c f7 f9 ....._.V..w.... + 00c3 - 14 b9 67 26 59 8e 77 e8-44 37 fe f5 47 c8 0e ..g&Y.w.D7..G.. + 00d2 - 88 35 ac 5a 2b 15 a5 7a-19 48 ca 3d 3c 0a 9e .5.Z+..z.H.=<.. + 00e1 - 58 ef b9 62 fb 0d af d1-2f cd ca 9f 18 0c 67 X..b..../.....g + 00f0 - 7a f3 a5 74 11 32 2d 8e-ef a5 d0 ae 37 cc 39 z..t.2-.....7.9 + 00ff - 7f . + crls: + + signerInfos: + version: 1 + d.issuerAndSerialNumber: + issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + serialNumber: 132769 + digestAlgorithm: + algorithm: sha256 (2.16.840.1.101.3.4.2.1) + parameter: + signedAttrs: + object: contentType (1.2.840.113549.1.9.3) + set: + OBJECT:pkcs7-data (1.2.840.113549.1.7.1) + + object: signingTime (1.2.840.113549.1.9.5) + set: + UTCTIME:Aug 22 10:16:48 2026 GMT + + object: messageDigest (1.2.840.113549.1.9.4) + set: + OCTET STRING: + 0000 - 26 d6 a9 e5 14 94 ec b6-b7 80 16 74 0f &..........t. + 000d - ba b7 3e ee c8 3e ff e4-cc 96 f0 db 09 ..>..>....... + 001a - 2f 4b 28 1e 5f 4c /K(._L + signatureAlgorithm: + algorithm: rsaEncryption (1.2.840.113549.1.1.1) + parameter: NULL + signature: + 0000 - 62 ac d9 a8 72 6c 00 cb-db 23 ae 44 c1 ce 75 b...rl...#.D..u + 000f - fe 51 e3 c7 fd f9 e6 38-6f 98 f8 82 c4 5c a8 .Q.....8o....\. + 001e - c9 12 f5 bf 14 6f a5 37-5c 15 86 24 e6 c5 e1 .....o.7\..$... + 002d - 98 0f 32 fc 6c 64 96 b2-f7 21 44 3f 91 0e bb ..2.ld...!D?... + 003c - 97 68 ce 27 42 df ea 85-95 03 07 7b 16 88 83 .h.'B......{... + 004b - c2 78 11 8d f6 9a ce 48-f9 0c 30 2a ef 07 46 .x.....H..0*..F + 005a - 58 00 33 39 0f b6 67 e3-55 6c a4 57 5c d2 64 X.39..g.Ul.W\.d + 0069 - 10 65 80 17 6d 48 a9 ad-56 02 5f 66 d5 92 2b .e..mH..V._f..+ + 0078 - b1 aa d9 d2 be c0 5e 7f-f8 72 7d 8e 92 2f aa ......^..r}../. + 0087 - 80 ff 75 10 b4 33 b1 08-be 7c f4 19 2d c1 02 ..u..3...|..-.. + 0096 - b2 1c 1d b2 c4 47 b5 8f-c3 94 60 9c a7 83 be .....G....`.... + 00a5 - 65 9c d7 62 7b 03 be d3-b2 a0 ef 0f e5 bc 37 e..b{.........7 + 00b4 - 71 eb 69 25 21 3f a2 39-e4 d4 8b 74 a5 99 63 q.i%!?.9...t..c + 00c3 - 71 3b a6 77 38 34 7d e9-8e e5 3b 5e 2b b4 8f q;.w84}...;^+.. + 00d2 - 0b 64 fd 83 91 52 66 8c-1a 63 10 4e d5 ba 2c .d...Rf..c.N.., + 00e1 - 12 db 09 d2 70 40 30 18-4f fe 56 d6 a7 67 bc ....p@0.O.V..g. + 00f0 - 26 2d 03 26 35 8b 29 c8-90 84 77 21 94 3d 87 &-.&5.)...w!.=. + 00ff - b1 . + unsignedAttrs: + +\n=== CAdES-BES invoice === +CMS_ContentInfo: + contentType: pkcs7-signedData (1.2.840.113549.1.7.2) + d.signedData: + version: 1 + digestAlgorithms: + algorithm: sha256 (2.16.840.1.101.3.4.2.1) + parameter: + encapContentInfo: + eContentType: pkcs7-data (1.2.840.113549.1.7.1) + eContent: + 0000 - 25 50 44 46 2d 31 2e 33-0a 25 93 8c 8b 9e 20 %PDF-1.3.%.... + 000f - 52 65 70 6f 72 74 4c 61-62 20 47 65 6e 65 72 ReportLab Gener + 001e - 61 74 65 64 20 50 44 46-20 64 6f 63 75 6d 65 ated PDF docume + 002d - 6e 74 20 28 6f 70 65 6e-73 6f 75 72 63 65 29 nt (opensource) + 003c - 0a 31 20 30 20 6f 62 6a-0a 3c 3c 0a 2f 46 31 .1 0 obj.<<./F1 + 004b - 20 32 20 30 20 52 20 2f-46 32 20 33 20 30 20 2 0 R /F2 3 0 + 005a - 52 20 2f 46 33 20 34 20-30 20 52 0a 3e 3e 0a R /F3 4 0 R.>>. + 0069 - 65 6e 64 6f 62 6a 0a 32-20 30 20 6f 62 6a 0a endobj.2 0 obj. + 0078 - 3c 3c 0a 2f 42 61 73 65-46 6f 6e 74 20 2f 48 <<./BaseFont /H + 0087 - 65 6c 76 65 74 69 63 61-20 2f 45 6e 63 6f 64 elvetica /Encod + 0096 - 69 6e 67 20 2f 57 69 6e-41 6e 73 69 45 6e 63 ing /WinAnsiEnc + 00a5 - 6f 64 69 6e 67 20 2f 4e-61 6d 65 20 2f 46 31 oding /Name /F1 + 00b4 - 20 2f 53 75 62 74 79 70-65 20 2f 54 79 70 65 /Subtype /Type + 00c3 - 31 20 2f 54 79 70 65 20-2f 46 6f 6e 74 0a 3e 1 /Type /Font.> + 00d2 - 3e 0a 65 6e 64 6f 62 6a-0a 33 20 30 20 6f 62 >.endobj.3 0 ob + 00e1 - 6a 0a 3c 3c 0a 2f 42 61-73 65 46 6f 6e 74 20 j.<<./BaseFont + 00f0 - 2f 48 65 6c 76 65 74 69-63 61 2d 42 6f 6c 64 /Helvetica-Bold + 00ff - 20 2f 45 6e 63 6f 64 69-6e 67 20 2f 57 69 6e /Encoding /Win + 010e - 41 6e 73 69 45 6e 63 6f-64 69 6e 67 20 2f 4e AnsiEncoding /N + 011d - 61 6d 65 20 2f 46 32 20-2f 53 75 62 74 79 70 ame /F2 /Subtyp + 012c - 65 20 2f 54 79 70 65 31-20 2f 54 79 70 65 20 e /Type1 /Type + 013b - 2f 46 6f 6e 74 0a 3e 3e-0a 65 6e 64 6f 62 6a /Font.>>.endobj + 014a - 0a 34 20 30 20 6f 62 6a-0a 3c 3c 0a 2f 42 61 .4 0 obj.<<./Ba + 0159 - 73 65 46 6f 6e 74 20 2f-48 65 6c 76 65 74 69 seFont /Helveti + 0168 - 63 61 2d 4f 62 6c 69 71-75 65 20 2f 45 6e 63 ca-Oblique /Enc + 0177 - 6f 64 69 6e 67 20 2f 57-69 6e 41 6e 73 69 45 oding /WinAnsiE + 0186 - 6e 63 6f 64 69 6e 67 20-2f 4e 61 6d 65 20 2f ncoding /Name / + 0195 - 46 33 20 2f 53 75 62 74-79 70 65 20 2f 54 79 F3 /Subtype /Ty + 01a4 - 70 65 31 20 2f 54 79 70-65 20 2f 46 6f 6e 74 pe1 /Type /Font + 01b3 - 0a 3e 3e 0a 65 6e 64 6f-62 6a 0a 35 20 30 20 .>>.endobj.5 0 + 01c2 - 6f 62 6a 0a 3c 3c 0a 2f-43 6f 6e 74 65 6e 74 obj.<<./Content + 01d1 - 73 20 39 20 30 20 52 20-2f 4d 65 64 69 61 42 s 9 0 R /MediaB + 01e0 - 6f 78 20 5b 20 30 20 30-20 35 39 35 2e 32 37 ox [ 0 0 595.27 + 01ef - 35 36 20 38 34 31 2e 38-38 39 38 20 5d 20 2f 56 841.8898 ] / + 01fe - 50 61 72 65 6e 74 20 38-20 30 20 52 20 2f 52 Parent 8 0 R /R + 020d - 65 73 6f 75 72 63 65 73-20 3c 3c 0a 2f 46 6f esources <<./Fo + 021c - 6e 74 20 31 20 30 20 52-20 2f 50 72 6f 63 53 nt 1 0 R /ProcS + 022b - 65 74 20 5b 20 2f 50 44-46 20 2f 54 65 78 74 et [ /PDF /Text + 023a - 20 2f 49 6d 61 67 65 42-20 2f 49 6d 61 67 65 /ImageB /Image + 0249 - 43 20 2f 49 6d 61 67 65-49 20 5d 0a 3e 3e 20 C /ImageI ].>> + 0258 - 2f 52 6f 74 61 74 65 20-30 20 2f 54 72 61 6e /Rotate 0 /Tran + 0267 - 73 20 3c 3c 0a 0a 3e 3e-20 0a 20 20 2f 54 79 s <<..>> . /Ty + 0276 - 70 65 20 2f 50 61 67 65-0a 3e 3e 0a 65 6e 64 pe /Page.>>.end + 0285 - 6f 62 6a 0a 36 20 30 20-6f 62 6a 0a 3c 3c 0a obj.6 0 obj.<<. + 0294 - 2f 50 61 67 65 4d 6f 64-65 20 2f 55 73 65 4e /PageMode /UseN + 02a3 - 6f 6e 65 20 2f 50 61 67-65 73 20 38 20 30 20 one /Pages 8 0 + 02b2 - 52 20 2f 54 79 70 65 20-2f 43 61 74 61 6c 6f R /Type /Catalo + 02c1 - 67 0a 3e 3e 0a 65 6e 64-6f 62 6a 0a 37 20 30 g.>>.endobj.7 0 + 02d0 - 20 6f 62 6a 0a 3c 3c 0a-2f 41 75 74 68 6f 72 obj.<<./Author + 02df - 20 28 4f 70 65 6e 41 49-20 2d 20 73 79 6e 74 (OpenAI - synt + 02ee - 68 65 74 69 63 20 74 65-73 74 20 66 69 78 74 hetic test fixt + 02fd - 75 72 65 29 20 2f 43 72-65 61 74 69 6f 6e 44 ure) /CreationD + 030c - 61 74 65 20 28 44 3a 32-30 32 36 30 38 32 32 ate (D:20260822 + 031b - 31 30 31 36 34 38 2b 30-30 27 30 30 27 29 20 101648+00'00') + 032a - 2f 43 72 65 61 74 6f 72-20 28 61 6e 6f 6e 79 /Creator (anony + 0339 - 6d 6f 75 73 29 20 2f 4b-65 79 77 6f 72 64 73 mous) /Keywords + 0348 - 20 28 29 20 2f 4d 6f 64-44 61 74 65 20 28 44 () /ModDate (D + 0357 - 3a 32 30 32 36 30 38 32-32 31 30 31 36 34 38 :20260822101648 + 0366 - 2b 30 30 27 30 30 27 29-20 2f 50 72 6f 64 75 +00'00') /Produ + 0375 - 63 65 72 20 28 52 65 70-6f 72 74 4c 61 62 20 cer (ReportLab + 0384 - 50 44 46 20 4c 69 62 72-61 72 79 20 2d 20 5c PDF Library - \ + 0393 - 28 6f 70 65 6e 73 6f 75-72 63 65 5c 29 29 20 (opensource\)) + 03a2 - 0a 20 20 2f 53 75 62 6a-65 63 74 20 28 4e 6f . /Subject (No + 03b1 - 6e 2d 70 72 6f 64 75 63-74 69 6f 6e 20 63 72 n-production cr + 03c0 - 79 70 74 6f 67 72 61 70-68 69 63 20 66 69 78 yptographic fix + 03cf - 74 75 72 65 29 20 2f 54-69 74 6c 65 20 28 53 ture) /Title (S + 03de - 79 6e 74 68 65 74 69 63-20 69 6e 76 6f 69 63 ynthetic invoic + 03ed - 65 20 66 69 78 74 75 72-65 20 66 6f 72 20 66 e fixture for f + 03fc - 69 6c 65 2d 76 69 65 77-65 72 20 69 73 73 75 ile-viewer issu + 040b - 65 20 32 30 36 29 20 2f-54 72 61 70 70 65 64 e 206) /Trapped + 041a - 20 2f 46 61 6c 73 65 0a-3e 3e 0a 65 6e 64 6f /False.>>.endo + 0429 - 62 6a 0a 38 20 30 20 6f-62 6a 0a 3c 3c 0a 2f bj.8 0 obj.<<./ + 0438 - 43 6f 75 6e 74 20 31 20-2f 4b 69 64 73 20 5b Count 1 /Kids [ + 0447 - 20 35 20 30 20 52 20 5d-20 2f 54 79 70 65 20 5 0 R ] /Type + 0456 - 2f 50 61 67 65 73 0a 3e-3e 0a 65 6e 64 6f 62 /Pages.>>.endob + 0465 - 6a 0a 39 20 30 20 6f 62-6a 0a 3c 3c 0a 2f 46 j.9 0 obj.<<./F + 0474 - 69 6c 74 65 72 20 5b 20-2f 41 53 43 49 49 38 ilter [ /ASCII8 + 0483 - 35 44 65 63 6f 64 65 20-2f 46 6c 61 74 65 44 5Decode /FlateD + 0492 - 65 63 6f 64 65 20 5d 20-2f 4c 65 6e 67 74 68 ecode ] /Length + 04a1 - 20 36 30 39 0a 3e 3e 0a-73 74 72 65 61 6d 0a 609.>>.stream. + 04b0 - 47 61 74 3d 68 63 26 55-37 2c 27 4c 71 69 67 Gat=hc&U7,'Lqig + 04bf - 27 52 27 6a 53 30 74 2d-5d 22 50 22 59 71 3e 'R'jS0t-]"P"Yq> + 04ce - 3a 5f 43 57 45 4d 35 3f-4b 4b 58 5c 69 4f 74 :_CWEM5?KKX\iOt + 04dd - 4e 60 32 43 55 61 25 5c-58 57 63 3d 2d 6f 57 N`2CUa%\XWc=-oW + 04ec - 55 72 39 6a 4d 63 39 38-60 3b 41 2d 31 43 48 Ur9jMc98`;A-1CH + 04fb - 41 39 6b 58 49 5e 64 65-55 74 2e 66 74 41 2a A9kXI^deUt.ftA* + 050a - 67 42 33 3a 2f 22 52 48-4d 5c 6c 73 5a 6b 6c gB3:/"RHM\lsZkl + 0519 - 2b 50 45 53 64 23 74 6b-33 4a 61 55 4f 41 2f +PESd#tk3JaUOA/ + 0528 - 27 49 4b 26 32 52 4d 28-39 66 6b 5d 5a 55 66 'IK&2RM(9fk]ZUf + 0537 - 56 59 52 4b 2a 54 31 60-33 24 5e 72 68 42 74 VYRK*T1`3$^rhBt + 0546 - 5e 22 49 6b 3a 4d 74 2f-49 28 54 3e 39 3d 5e ^"Ik:Mt/I(T>9=^ + 0555 - 21 50 3d 29 32 42 68 32-55 49 37 50 54 65 42 !P=)2Bh2UI7PTeB + 0564 - 37 44 4e 3d 5e 44 2e 29-3a 36 27 46 29 29 24 7DN=^D.):6'F))$ + 0573 - 5f 2d 6b 32 51 4d 49 30-63 31 42 4c 67 46 4c _-k2QMI0c1BLgFL + 0582 - 52 69 71 64 2e 54 62 40-5b 66 4a 5f 52 66 27 Riqd.Tb@[fJ_Rf' + 0591 - 41 2a 5b 2f 43 68 47 2a-21 40 46 38 3f 45 6c A*[/ChG*!@F8?El + 05a0 - 50 4d 44 2d 41 62 4e 52-42 65 3c 5b 54 4a 27 PMD-AbNRBe<[TJ' + 05af - 21 56 50 4b 2e 73 37 6d-37 69 60 5b 48 68 50 !VPK.s7m7i`[HhP + 05be - 48 57 49 4e 60 6c 44 41-68 56 22 2c 60 39 42 HWIN`lDAhV",`9B + 05cd - 40 6e 61 51 25 67 72 29-21 38 59 26 60 50 5e @naQ%gr)!8Y&`P^ + 05dc - 24 30 2a 44 41 5f 4e 5c-5d 69 26 22 53 35 43 $0*DA_N\]i&"S5C + 05eb - 54 27 5e 25 6b 27 59 35-42 72 52 5f 41 27 5e T'^%k'Y5BrR_A'^ + 05fa - 2a 30 44 72 21 28 4e 28-73 65 3c 71 6a 3d 5b *0Dr!(N(semgGj4g:ilXkT + 0663 - 71 5c 63 53 2f 3f 4d 54-4c 38 4a 64 41 37 24 q\cS/?MTL8JdA7$ + 0672 - 36 3d 41 4f 3c 63 3a 64-34 2e 36 59 50 5d 47 6=AO//27OijTj?8gc- + 069f - 46 5b 39 69 4e 27 6e 2e-3e 57 38 39 6c 37 45 F[9iN'n.>W89l7E + 06ae - 43 57 2d 64 30 44 56 4c-70 63 23 43 21 72 29 CW-d0DVLpc#C!r) + 06bd - 4b 32 66 4c 31 38 49 63-6e 4e 5e 5f 67 58 64 K2fL18IcnN^_gXd + 06cc - 4c 26 5f 22 5f 54 4b 60-60 21 3a 34 3f 56 4d L&_"_TK``!:4?VM + 06db - 39 5c 6a 69 55 22 64 43-33 3d 37 6c 65 46 5d 9\jiU"dC3=7leF] + 06ea - 5b 6f 34 72 45 49 31 47-66 37 51 35 73 55 4c [o4rEI1Gf7Q5sUL + 06f9 - 4f 32 41 4a 49 6e 24 71-49 6d 41 48 33 37 55 O2AJIn$qImAH37U + 0708 - 21 46 3d 2d 2b 68 23 7e-3e 65 6e 64 73 74 72 !F=-+h#~>endstr + 0717 - 65 61 6d 0a 65 6e 64 6f-62 6a 0a 78 72 65 66 eam.endobj.xref + 0726 - 0a 30 20 31 30 0a 30 30-30 30 30 30 30 30 30 .0 10.000000000 + 0735 - 30 20 36 35 35 33 35 20-66 20 0a 30 30 30 30 0 65535 f .0000 + 0744 - 30 30 30 30 36 31 20 30-30 30 30 30 20 6e 20 000061 00000 n + 0753 - 0a 30 30 30 30 30 30 30-31 31 32 20 30 30 30 .0000000112 000 + 0762 - 30 30 20 6e 20 0a 30 30-30 30 30 30 30 32 31 00 n .000000021 + 0771 - 39 20 30 30 30 30 30 20-6e 20 0a 30 30 30 30 9 00000 n .0000 + 0780 - 30 30 30 33 33 31 20 30-30 30 30 30 20 6e 20 000331 00000 n + 078f - 0a 30 30 30 30 30 30 30-34 34 36 20 30 30 30 .0000000446 000 + 079e - 30 30 20 6e 20 0a 30 30-30 30 30 30 30 36 34 00 n .000000064 + 07ad - 39 20 30 30 30 30 30 20-6e 20 0a 30 30 30 30 9 00000 n .0000 + 07bc - 30 30 30 37 31 37 20 30-30 30 30 30 20 6e 20 000717 00000 n + 07cb - 0a 30 30 30 30 30 30 31-30 36 38 20 30 30 30 .0000001068 000 + 07da - 30 30 20 6e 20 0a 30 30-30 30 30 30 31 31 32 00 n .000000112 + 07e9 - 37 20 30 30 30 30 30 20-6e 20 0a 74 72 61 69 7 00000 n .trai + 07f8 - 6c 65 72 0a 3c 3c 0a 2f-49 44 20 0a 5b 3c 31 ler.<<./ID .[<1 + 0807 - 66 32 34 61 36 30 39 63-36 62 34 61 61 65 36 f24a609c6b4aae6 + 0816 - 35 61 35 39 38 33 30 35-63 38 37 66 34 30 30 5a598305c87f400 + 0825 - 30 3e 3c 31 66 32 34 61-36 30 39 63 36 62 34 0><1f24a609c6b4 + 0834 - 61 61 65 36 35 61 35 39-38 33 30 35 63 38 37 aae65a598305c87 + 0843 - 66 34 30 30 30 3e 5d 0a-25 20 52 65 70 6f 72 f4000>].% Repor + 0852 - 74 4c 61 62 20 67 65 6e-65 72 61 74 65 64 20 tLab generated + 0861 - 50 44 46 20 64 6f 63 75-6d 65 6e 74 20 2d 2d PDF document -- + 0870 - 20 64 69 67 65 73 74 20-28 6f 70 65 6e 73 6f digest (openso + 087f - 75 72 63 65 29 0a 0a 2f-49 6e 66 6f 20 37 20 urce)../Info 7 + 088e - 30 20 52 0a 2f 52 6f 6f-74 20 36 20 30 20 52 0 R./Root 6 0 R + 089d - 0a 2f 53 69 7a 65 20 31-30 0a 3e 3e 0a 73 74 ./Size 10.>>.st + 08ac - 61 72 74 78 72 65 66 0a-31 38 32 36 0a 25 25 artxref.1826.%% + 08bb - 45 4f 46 0a EOF. + certificates: + d.certificate: + cert_info: + version: 2 + serialNumber: 0x3371BBFB038BEFBA9876B9EFBC1D99059AAF2AE5 + signature: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + validity: + notBefore: Aug 22 10:16:48 2026 GMT + notAfter: Aug 22 10:16:48 2046 GMT + subject: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + key: X509_PUBKEY: + algor: + algorithm: rsaEncryption (1.2.840.113549.1.1.1) + parameter: NULL + public_key: (0 unused bits) + 0000 - 30 82 01 0a 02 82 01 01-00 af 4b 5e 0c 8e 0.........K^.. + 000e - d8 e5 5d e1 22 61 54 14-1f ce 33 34 fc e8 ..]."aT...34.. + 001c - 3d 38 c1 73 ac b2 65 cf-e9 bd 16 c9 9b 23 =8.s..e......# + 002a - 1c 07 3b 08 5d 19 3c bc-d6 52 b9 52 b6 df ..;.].<..R.R.. + 0038 - ae 5a e3 d6 8c 9e 8c 82-62 7d 95 64 79 3f .Z......b}.dy? + 0046 - e8 f5 bf 23 2b 16 18 db-3d b9 ba 27 31 0b ...#+...=..'1. + 0054 - 84 d2 01 97 70 7d 81 58-67 72 f6 47 e9 cb ....p}.Xgr.G.. + 0062 - 19 53 e4 69 49 a4 e3 f9-07 a7 55 be c3 41 .S.iI.....U..A + 0070 - c0 b5 13 03 c7 ac 3d 2f-c4 89 0e 54 76 73 ......=/...Tvs + 007e - fd 4a 89 c3 29 f0 70 17-4d 83 7b 6f b7 10 .J..).p.M.{o.. + 008c - 10 df 3e 16 fd db 73 8f-9b 46 4f 8b d9 30 ..>...s..FO..0 + 009a - ed 98 9b 3b da 22 33 d4-e4 37 fc e9 ee 0d ...;."3..7.... + 00a8 - a3 2b 0a 33 5d 2e e8 8c-b6 97 46 5a 53 0b .+.3].....FZS. + 00b6 - ed 61 ff 35 84 64 74 c3-59 70 f0 4b a5 3f .a.5.dt.Yp.K.? + 00c4 - b0 eb 31 41 8f d9 ef 02-55 c8 ea 31 f7 bc ..1A....U..1.. + 00d2 - a0 c8 25 f1 fd 41 0a c6-e3 ff 19 58 80 cc ..%..A.....X.. + 00e0 - ba 27 5c ff b3 41 c4 67-33 60 e5 2a 7b 41 .'\..A.g3`.*{A + 00ee - 59 70 88 1b 34 f4 e6 68-d4 16 6b 42 e6 72 Yp..4..h..kB.r + 00fc - 7c af a6 4b 20 0d 9d 59-8f 6b e6 c2 db 02 |..K ..Y.k.... + 010a - 03 01 00 01 .... + issuerUID: + subjectUID: + extensions: + object: X509v3 Authority Key Identifier (2.5.29.35) + critical: FALSE + value: + 0000 - 30 16 80 14 93 bc 0b 0f-98 7f 2d 80 e1 0.........-.. + 000d - 8c bf 5d f4 53 f9 ed 01-a9 2c cf ..].S....,. + + object: X509v3 Basic Constraints (2.5.29.19) + critical: TRUE + value: + 0000 - 30 06 01 01 ff 02 01 01- 0....... + + object: X509v3 Key Usage (2.5.29.15) + critical: TRUE + value: + 0000 - 03 02 01 06 .... + + object: X509v3 Subject Key Identifier (2.5.29.14) + critical: FALSE + value: + 0000 - 04 14 93 bc 0b 0f 98 7f-2d 80 e1 8c bf ........-.... + 000d - 5d f4 53 f9 ed 01 a9 2c-cf ].S....,. + sig_alg: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + signature: (0 unused bits) + 0000 - 5d 0b aa 6b 92 25 b9 c6-bd 4c 06 20 11 12 75 ]..k.%...L. ..u + 000f - 65 f9 21 b4 f4 21 06 c4-26 26 c2 3e 37 ca cc e.!..!..&&.>7.. + 001e - 2e 6d 83 b2 15 b8 9b c2-47 d9 a0 47 7a c4 a9 .m......G..Gz.. + 002d - a7 90 0c 30 2a 0c 70 c5-12 c2 a6 fa 01 74 b5 ...0*.p......t. + 003c - 91 e4 65 9b b7 a6 8e 93-db 6a bb 6b 0c bb f5 ..e......j.k... + 004b - ef 83 3a eb ba 24 9a 8f-b6 f1 82 f8 c7 41 2c ..:..$.......A, + 005a - 3d 99 14 96 ae 82 83 be-29 2b 87 5d 96 66 fd =.......)+.].f. + 0069 - e5 e9 09 70 29 eb 2e a1-01 04 42 7f 85 81 db ...p).....B.... + 0078 - cc 18 8f b6 a9 64 3a dc-3e 97 93 fe 94 36 90 .....d:.>....6. + 0087 - 21 8e 27 b6 c7 21 eb f9-0f c4 cf 04 79 84 a2 !.'..!......y.. + 0096 - 11 16 8b 16 9c ae af 20-c1 0b 08 de f6 56 84 ....... .....V. + 00a5 - 5d 61 44 fe 38 1c 24 f4-96 8e ac 19 93 6c 4f ]aD.8.$......lO + 00b4 - a2 c8 d0 a6 6e 3e a0 1e-9f f8 1c e7 74 d8 5b ....n>......t.[ + 00c3 - a9 55 51 43 25 74 17 77-5a 56 97 b2 09 84 8f .UQC%t.wZV..... + 00d2 - f4 ed 7b b4 47 6c f7 87-c4 da 3f 5d 16 f3 e8 ..{.Gl....?]... + 00e1 - 07 28 7d ea 09 93 d2 0d-43 af 8a 9c e6 b9 da .(}.....C...... + 00f0 - 6d 76 98 12 b5 8a 31 6b-2c 84 f6 79 20 4a 08 mv....1k,..y J. + 00ff - 96 . + + d.certificate: + cert_info: + version: 2 + serialNumber: 132769 + signature: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + validity: + notBefore: Aug 22 10:16:48 2026 GMT + notAfter: Aug 17 10:16:48 2046 GMT + subject: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=CMS Test Signer Alice, emailAddress=alice@example.invalid + key: X509_PUBKEY: + algor: + algorithm: rsaEncryption (1.2.840.113549.1.1.1) + parameter: NULL + public_key: (0 unused bits) + 0000 - 30 82 01 0a 02 82 01 01-00 c6 32 7b 59 a4 0.........2{Y. + 000e - 98 14 ff 12 83 e6 e7 49-df b3 b5 e6 ee 2b .......I.....+ + 001c - c1 7b 33 31 67 3d e1 f4-c9 52 7b b5 6f 6e .{31g=...R{.on + 002a - 40 a9 7b 34 ea 75 ea 0b-04 a2 81 c5 20 7e @.{4.u...... ~ + 0038 - 6b 95 61 cf d7 01 38 05-5a 29 d7 38 30 47 k.a...8.Z).80G + 0046 - 6e ca 2b af d2 5a 25 fd-d1 96 5a 8a 34 0e n.+..Z%...Z.4. + 0054 - e2 61 35 de 72 2c e4 8c-5e c9 7d da 46 86 .a5.r,..^.}.F. + 0062 - 94 9f 8a f6 07 02 61 cf-e4 59 8c 53 10 5a ......a..Y.S.Z + 0070 - a2 33 b6 05 06 27 94 54-1a a8 0f 9b 80 a1 .3...'.T...... + 007e - 43 94 a2 56 a9 fc 92 ba-42 48 bb a3 df db C..V....BH.... + 008c - 14 10 cc c0 ef 40 21 65-0e c4 0c e3 b6 f7 .....@!e...... + 009a - 53 4b d0 fe 4e 24 26 e1-a7 8c 7c 04 39 64 SK..N$&...|.9d + 00a8 - 52 7b 14 34 29 aa 80 f2-91 58 2f f2 81 b5 R{.4)....X/... + 00b6 - 46 23 f2 e6 d4 67 65 6e-35 56 d3 17 07 21 F#...gen5V...! + 00c4 - 60 aa f2 21 e0 8f ac 9b-2c 67 36 1a e3 72 `..!....,g6..r + 00d2 - c3 f8 05 e1 9a 28 af 0e-f1 cd 47 a8 88 1f .....(....G... + 00e0 - 6e 02 53 ee fd 1a b9 e4-d3 e3 df cb 3c f5 n.S.........<. + 00ee - 61 c7 8b e9 f2 28 f3 26-ed 10 59 d2 4a 26 a....(.&..Y.J& + 00fc - 95 ce 34 52 57 ac f7 50-ba 4f f8 45 b9 02 ..4RW..P.O.E.. + 010a - 03 01 00 01 .... + issuerUID: + subjectUID: + extensions: + object: X509v3 Basic Constraints (2.5.29.19) + critical: TRUE + value: + 0000 - 30 00 0. + + object: X509v3 Key Usage (2.5.29.15) + critical: TRUE + value: + 0000 - 03 02 07 80 .... + + object: X509v3 Extended Key Usage (2.5.29.37) + critical: FALSE + value: + 0000 - 30 0a 06 08 2b 06 01 05-05 07 03 04 0...+....... + + object: X509v3 Subject Key Identifier (2.5.29.14) + critical: FALSE + value: + 0000 - 04 14 d6 93 b0 2f ea ed-46 ed aa 1c 9b ...../..F.... + 000d - 46 eb b9 e0 a1 56 6f 26-61 F....Vo&a + + object: X509v3 Authority Key Identifier (2.5.29.35) + critical: FALSE + value: + 0000 - 30 16 80 14 93 bc 0b 0f-98 7f 2d 80 e1 0.........-.. + 000d - 8c bf 5d f4 53 f9 ed 01-a9 2c cf ..].S....,. + + object: X509v3 Certificate Policies (2.5.29.32) + critical: FALSE + value: + 0000 - 30 0f 30 0d 06 0b 2b 06-01 04 01 83 b2 0.0...+...... + 000d - 03 81 4e 0a ..N. + sig_alg: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + signature: (0 unused bits) + 0000 - 66 7f 9e 84 1d bf f2 01-38 a9 52 77 6e e9 15 f.......8.Rwn.. + 000f - 53 fd 12 cd 60 13 6d cc-01 c6 25 e1 88 c5 48 S...`.m...%...H + 001e - 7a 31 5d bb 81 88 98 e9-10 74 71 48 06 63 d3 z1]......tqH.c. + 002d - 14 00 21 d3 c1 db 49 20-aa 87 f8 71 6c a0 9e ..!...I ...ql.. + 003c - 34 23 d7 fa 25 d9 b2 75-6c ce 29 b4 6e 88 ca 4#..%..ul.).n.. + 004b - 26 2b d2 0e 36 df 7e ba-58 11 73 49 83 fa 8d &+..6.~.X.sI... + 005a - ac 31 6e d2 ff 84 ab 4b-72 36 0b e0 8f 2a 13 .1n....Kr6...*. + 0069 - 3f 0c b6 a5 a0 4e 39 a8-d6 11 da 97 e3 bd 79 ?....N9.......y + 0078 - 0e 1d 31 37 43 7f 90 6b-c3 8e 9b 65 b6 77 bb ..17C..k...e.w. + 0087 - 12 3c 09 f5 28 c1 b7 2b-ae 50 70 04 2d c4 81 .<..(..+.Pp.-.. + 0096 - 26 34 cc 57 b1 90 e1 6b-f8 f3 5c 16 eb ba 82 &4.W...k..\.... + 00a5 - 13 e4 0a 0b 7c 06 ab 65-d6 5e e1 50 2c 3c c3 ....|..e.^.P,<. + 00b4 - c2 e7 08 97 82 5f 9f 56-80 0f 77 b2 9c f7 f9 ....._.V..w.... + 00c3 - 14 b9 67 26 59 8e 77 e8-44 37 fe f5 47 c8 0e ..g&Y.w.D7..G.. + 00d2 - 88 35 ac 5a 2b 15 a5 7a-19 48 ca 3d 3c 0a 9e .5.Z+..z.H.=<.. + 00e1 - 58 ef b9 62 fb 0d af d1-2f cd ca 9f 18 0c 67 X..b..../.....g + 00f0 - 7a f3 a5 74 11 32 2d 8e-ef a5 d0 ae 37 cc 39 z..t.2-.....7.9 + 00ff - 7f . + crls: + + signerInfos: + version: 1 + d.issuerAndSerialNumber: + issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + serialNumber: 132769 + digestAlgorithm: + algorithm: sha256 (2.16.840.1.101.3.4.2.1) + parameter: + signedAttrs: + object: contentType (1.2.840.113549.1.9.3) + set: + OBJECT:pkcs7-data (1.2.840.113549.1.7.1) + + object: signingTime (1.2.840.113549.1.9.5) + set: + UTCTIME:Aug 22 10:16:48 2026 GMT + + object: messageDigest (1.2.840.113549.1.9.4) + set: + OCTET STRING: + 0000 - 26 d6 a9 e5 14 94 ec b6-b7 80 16 74 0f &..........t. + 000d - ba b7 3e ee c8 3e ff e4-cc 96 f0 db 09 ..>..>....... + 001a - 2f 4b 28 1e 5f 4c /K(._L + + object: id-smime-aa-signingCertificateV2 (1.2.840.113549.1.9.16.2.47) + set: + SEQUENCE: + 0:d=0 hl=3 l= 158 cons: SEQUENCE + 3:d=1 hl=3 l= 155 cons: SEQUENCE + 6:d=2 hl=3 l= 152 cons: SEQUENCE + 9:d=3 hl=2 l= 32 prim: OCTET STRING [HEX DUMP]:DD023CE6105F0BC003A33422A25B60801C9A63A64DC55F045A90F1F54D743A5E + 43:d=3 hl=2 l= 116 cons: SEQUENCE + 45:d=4 hl=2 l= 109 cons: SEQUENCE + 47:d=5 hl=2 l= 107 cons: cont [ 4 ] + 49:d=6 hl=2 l= 105 cons: SEQUENCE + 51:d=7 hl=2 l= 11 cons: SET + 53:d=8 hl=2 l= 9 cons: SEQUENCE + 55:d=9 hl=2 l= 3 prim: OBJECT :countryName + 60:d=9 hl=2 l= 2 prim: PRINTABLESTRING :IT + 64:d=7 hl=2 l= 34 cons: SET + 66:d=8 hl=2 l= 32 cons: SEQUENCE + 68:d=9 hl=2 l= 3 prim: OBJECT :organizationName + 73:d=9 hl=2 l= 25 prim: UTF8STRING :File Viewer Test Fixtures + 100:d=7 hl=2 l= 21 cons: SET + 102:d=8 hl=2 l= 19 cons: SEQUENCE + 104:d=9 hl=2 l= 3 prim: OBJECT :organizationalUnitName + 109:d=9 hl=2 l= 12 prim: UTF8STRING :Testing Only + 123:d=7 hl=2 l= 31 cons: SET + 125:d=8 hl=2 l= 29 cons: SEQUENCE + 127:d=9 hl=2 l= 3 prim: OBJECT :commonName + 132:d=9 hl=2 l= 22 prim: UTF8STRING :Issue 206 Test Root CA + 156:d=4 hl=2 l= 3 prim: INTEGER :0206A1 + signatureAlgorithm: + algorithm: rsaEncryption (1.2.840.113549.1.1.1) + parameter: NULL + signature: + 0000 - 36 5d d0 37 4d 43 97 dd-d1 bf 35 0f 9f 32 b5 6].7MC....5..2. + 000f - 95 67 cb 36 29 56 cd 2c-da 25 05 cf dd 2b 67 .g.6)V.,.%...+g + 001e - a6 84 e6 0f 05 fc 56 2a-65 2b 88 ef 1d 6f 00 ......V*e+...o. + 002d - dc 77 e1 ed 7d 45 b0 4f-fc c5 ac 20 6c 50 28 .w..}E.O... lP( + 003c - fd 76 86 7b 8f b0 7f b2-ce 7f 9c f5 03 c4 e1 .v.{........... + 004b - 78 ec a5 83 83 84 ca 69-a4 a0 1f 76 12 86 c6 x......i...v... + 005a - f1 27 16 15 92 33 84 14-dc ca 2b f6 ca 59 e1 .'...3....+..Y. + 0069 - 29 ad c0 df f1 49 f5 0d-dc 0a 5b a2 61 ea c5 )....I....[.a.. + 0078 - 3d fc cf 5b c0 20 1c 94-66 b5 44 95 b7 77 5d =..[. ..f.D..w] + 0087 - 46 7c 69 0a 1a a4 a9 c8-7f 0b 51 32 fa 36 49 F|i.......Q2.6I + 0096 - 95 e9 0f ab 70 86 e3 82-2b 9a 8d 80 6d 71 c9 ....p...+...mq. + 00a5 - 7c 28 e0 11 bf 03 82 e9-7a 4c 3e f7 3e dd 01 |(......zL>.>.. + 00b4 - 30 04 64 f2 4a ef 0f 91-fd 5e 7b c8 15 ef 45 0.d.J....^{...E + 00c3 - 18 2f a5 7a 97 54 8e 6d-b3 a1 ae 2a c9 fd 4e ./.z.T.m...*..N + 00d2 - f4 0e 4a 88 27 3d d5 2f-dd aa 7f c3 56 10 ca ..J.'=./....V.. + 00e1 - 89 9f 75 52 29 0c e7 cf-67 30 d7 a8 1e 54 d1 ..uR)...g0...T. + 00f0 - 29 e3 f0 99 db d8 0f 4c-70 d0 f0 af c5 f3 22 )......Lp....." + 00ff - 4f O + unsignedAttrs: + +\n=== CMS multi-signer === +CMS_ContentInfo: + contentType: pkcs7-signedData (1.2.840.113549.1.7.2) + d.signedData: + version: 1 + digestAlgorithms: + algorithm: sha384 (2.16.840.1.101.3.4.2.2) + parameter: + encapContentInfo: + eContentType: pkcs7-data (1.2.840.113549.1.7.1) + eContent: + 0000 - 46 69 6c 65 20 56 69 65-77 65 72 20 69 73 73 File Viewer iss + 000f - 75 65 20 32 30 36 20 73-79 6e 74 68 65 74 69 ue 206 syntheti + 001e - 63 20 66 69 78 74 75 72-65 0d 0a 54 68 69 73 c fixture..This + 002d - 20 63 6f 6e 74 65 6e 74-20 69 73 20 73 69 67 content is sig + 003c - 6e 65 64 20 62 79 20 74-77 6f 20 74 65 73 74 ned by two test + 004b - 20 73 69 67 6e 65 72 73-2e 0d 0a signers... + certificates: + d.certificate: + cert_info: + version: 2 + serialNumber: 132786 + signature: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + validity: + notBefore: Aug 22 10:16:48 2026 GMT + notAfter: Aug 17 10:16:48 2046 GMT + subject: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=CMS Test Signer Bob, emailAddress=bob@example.invalid + key: X509_PUBKEY: + algor: + algorithm: id-ecPublicKey (1.2.840.10045.2.1) + parameter: OBJECT:prime256v1 (1.2.840.10045.3.1.7) + public_key: (0 unused bits) + 0000 - 04 39 1b 70 65 e0 f8 e0-9d 80 d4 2b 57 63 .9.pe......+Wc + 000e - 7f a1 16 5e f6 11 1d 4d-b7 6b 8e 53 0b 74 ...^...M.k.S.t + 001c - 94 0b ee 96 2b 7c 7b 2a-02 ab 38 1c fa 25 ....+|{*..8..% + 002a - 39 e7 3b 88 ff 11 2e 63-15 71 66 67 a1 20 9.;....c.qfg. + 0038 - c4 99 91 44 47 20 c6 51-11 ...DG .Q. + issuerUID: + subjectUID: + extensions: + object: X509v3 Basic Constraints (2.5.29.19) + critical: TRUE + value: + 0000 - 30 00 0. + + object: X509v3 Key Usage (2.5.29.15) + critical: TRUE + value: + 0000 - 03 02 07 80 .... + + object: X509v3 Extended Key Usage (2.5.29.37) + critical: FALSE + value: + 0000 - 30 0a 06 08 2b 06 01 05-05 07 03 04 0...+....... + + object: X509v3 Subject Key Identifier (2.5.29.14) + critical: FALSE + value: + 0000 - 04 14 08 36 de 11 38 00-c0 83 e2 a9 4f ...6..8.....O + 000d - 41 13 83 47 12 fc 2c 48-da A..G..,H. + + object: X509v3 Authority Key Identifier (2.5.29.35) + critical: FALSE + value: + 0000 - 30 16 80 14 93 bc 0b 0f-98 7f 2d 80 e1 0.........-.. + 000d - 8c bf 5d f4 53 f9 ed 01-a9 2c cf ..].S....,. + + object: X509v3 Certificate Policies (2.5.29.32) + critical: FALSE + value: + 0000 - 30 0f 30 0d 06 0b 2b 06-01 04 01 83 b2 0.0...+...... + 000d - 03 81 4e 0a ..N. + sig_alg: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + signature: (0 unused bits) + 0000 - a9 48 84 f1 99 00 b8 d3-65 73 d4 6e 78 89 b2 .H......es.nx.. + 000f - fc 5d 0d 3d 19 53 82 84-01 5e 4e bc 63 f9 33 .].=.S...^N.c.3 + 001e - 16 6b 84 78 29 fe 48 c1-e4 ff c2 1c 6b 8c fb .k.x).H.....k.. + 002d - bf 09 4e 93 5d 42 e0 72-0b 3f ed 68 2b 50 22 ..N.]B.r.?.h+P" + 003c - a1 91 3a 5f 05 cc 77 7e-25 99 95 5f e8 2b 82 ..:_..w~%.._.+. + 004b - 24 37 3d a6 dc 14 1c 28-34 be 18 27 39 2d 02 $7=....(4..'9-. + 005a - 74 8d 20 ef a4 ab b0 ef-d8 1b ea eb da ea 02 t. ............ + 0069 - e1 03 07 cd 64 06 b6 8d-23 53 02 6b a4 9b 9f ....d...#S.k... + 0078 - 2d 52 22 cc 28 cf 35 2f-17 5f 97 7f 31 1a 6a -R".(.5/._..1.j + 0087 - 25 1f 97 df c1 2b 89 79-fa 2f c0 d3 ba 10 0d %....+.y./..... + 0096 - f2 84 6c 65 2f 78 16 db-6f da 84 f9 f6 6d af ..le/x..o....m. + 00a5 - 6a fa a3 38 d7 17 4f ea-25 72 69 fd 48 c5 b3 j..8..O.%ri.H.. + 00b4 - e1 c0 1c c0 7d 2d 68 9a-b6 38 39 74 cd 04 9a ....}-h..89t... + 00c3 - 91 61 a7 fb d4 cc 42 55-29 dc 5e 47 e9 a1 62 .a....BU).^G..b + 00d2 - b9 cb 87 b1 b9 28 f7 75-81 ea e8 84 08 ef 69 .....(.u......i + 00e1 - b7 08 99 fc 30 51 15 54-06 3f d8 ef 88 b7 76 ....0Q.T.?....v + 00f0 - 9f 22 00 27 e9 c5 c1 51-a6 54 31 70 4e d8 b6 .".'...Q.T1pN.. + 00ff - 0f . + + d.certificate: + cert_info: + version: 2 + serialNumber: 0x3371BBFB038BEFBA9876B9EFBC1D99059AAF2AE5 + signature: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + validity: + notBefore: Aug 22 10:16:48 2026 GMT + notAfter: Aug 22 10:16:48 2046 GMT + subject: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + key: X509_PUBKEY: + algor: + algorithm: rsaEncryption (1.2.840.113549.1.1.1) + parameter: NULL + public_key: (0 unused bits) + 0000 - 30 82 01 0a 02 82 01 01-00 af 4b 5e 0c 8e 0.........K^.. + 000e - d8 e5 5d e1 22 61 54 14-1f ce 33 34 fc e8 ..]."aT...34.. + 001c - 3d 38 c1 73 ac b2 65 cf-e9 bd 16 c9 9b 23 =8.s..e......# + 002a - 1c 07 3b 08 5d 19 3c bc-d6 52 b9 52 b6 df ..;.].<..R.R.. + 0038 - ae 5a e3 d6 8c 9e 8c 82-62 7d 95 64 79 3f .Z......b}.dy? + 0046 - e8 f5 bf 23 2b 16 18 db-3d b9 ba 27 31 0b ...#+...=..'1. + 0054 - 84 d2 01 97 70 7d 81 58-67 72 f6 47 e9 cb ....p}.Xgr.G.. + 0062 - 19 53 e4 69 49 a4 e3 f9-07 a7 55 be c3 41 .S.iI.....U..A + 0070 - c0 b5 13 03 c7 ac 3d 2f-c4 89 0e 54 76 73 ......=/...Tvs + 007e - fd 4a 89 c3 29 f0 70 17-4d 83 7b 6f b7 10 .J..).p.M.{o.. + 008c - 10 df 3e 16 fd db 73 8f-9b 46 4f 8b d9 30 ..>...s..FO..0 + 009a - ed 98 9b 3b da 22 33 d4-e4 37 fc e9 ee 0d ...;."3..7.... + 00a8 - a3 2b 0a 33 5d 2e e8 8c-b6 97 46 5a 53 0b .+.3].....FZS. + 00b6 - ed 61 ff 35 84 64 74 c3-59 70 f0 4b a5 3f .a.5.dt.Yp.K.? + 00c4 - b0 eb 31 41 8f d9 ef 02-55 c8 ea 31 f7 bc ..1A....U..1.. + 00d2 - a0 c8 25 f1 fd 41 0a c6-e3 ff 19 58 80 cc ..%..A.....X.. + 00e0 - ba 27 5c ff b3 41 c4 67-33 60 e5 2a 7b 41 .'\..A.g3`.*{A + 00ee - 59 70 88 1b 34 f4 e6 68-d4 16 6b 42 e6 72 Yp..4..h..kB.r + 00fc - 7c af a6 4b 20 0d 9d 59-8f 6b e6 c2 db 02 |..K ..Y.k.... + 010a - 03 01 00 01 .... + issuerUID: + subjectUID: + extensions: + object: X509v3 Authority Key Identifier (2.5.29.35) + critical: FALSE + value: + 0000 - 30 16 80 14 93 bc 0b 0f-98 7f 2d 80 e1 0.........-.. + 000d - 8c bf 5d f4 53 f9 ed 01-a9 2c cf ..].S....,. + + object: X509v3 Basic Constraints (2.5.29.19) + critical: TRUE + value: + 0000 - 30 06 01 01 ff 02 01 01- 0....... + + object: X509v3 Key Usage (2.5.29.15) + critical: TRUE + value: + 0000 - 03 02 01 06 .... + + object: X509v3 Subject Key Identifier (2.5.29.14) + critical: FALSE + value: + 0000 - 04 14 93 bc 0b 0f 98 7f-2d 80 e1 8c bf ........-.... + 000d - 5d f4 53 f9 ed 01 a9 2c-cf ].S....,. + sig_alg: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + signature: (0 unused bits) + 0000 - 5d 0b aa 6b 92 25 b9 c6-bd 4c 06 20 11 12 75 ]..k.%...L. ..u + 000f - 65 f9 21 b4 f4 21 06 c4-26 26 c2 3e 37 ca cc e.!..!..&&.>7.. + 001e - 2e 6d 83 b2 15 b8 9b c2-47 d9 a0 47 7a c4 a9 .m......G..Gz.. + 002d - a7 90 0c 30 2a 0c 70 c5-12 c2 a6 fa 01 74 b5 ...0*.p......t. + 003c - 91 e4 65 9b b7 a6 8e 93-db 6a bb 6b 0c bb f5 ..e......j.k... + 004b - ef 83 3a eb ba 24 9a 8f-b6 f1 82 f8 c7 41 2c ..:..$.......A, + 005a - 3d 99 14 96 ae 82 83 be-29 2b 87 5d 96 66 fd =.......)+.].f. + 0069 - e5 e9 09 70 29 eb 2e a1-01 04 42 7f 85 81 db ...p).....B.... + 0078 - cc 18 8f b6 a9 64 3a dc-3e 97 93 fe 94 36 90 .....d:.>....6. + 0087 - 21 8e 27 b6 c7 21 eb f9-0f c4 cf 04 79 84 a2 !.'..!......y.. + 0096 - 11 16 8b 16 9c ae af 20-c1 0b 08 de f6 56 84 ....... .....V. + 00a5 - 5d 61 44 fe 38 1c 24 f4-96 8e ac 19 93 6c 4f ]aD.8.$......lO + 00b4 - a2 c8 d0 a6 6e 3e a0 1e-9f f8 1c e7 74 d8 5b ....n>......t.[ + 00c3 - a9 55 51 43 25 74 17 77-5a 56 97 b2 09 84 8f .UQC%t.wZV..... + 00d2 - f4 ed 7b b4 47 6c f7 87-c4 da 3f 5d 16 f3 e8 ..{.Gl....?]... + 00e1 - 07 28 7d ea 09 93 d2 0d-43 af 8a 9c e6 b9 da .(}.....C...... + 00f0 - 6d 76 98 12 b5 8a 31 6b-2c 84 f6 79 20 4a 08 mv....1k,..y J. + 00ff - 96 . + + d.certificate: + cert_info: + version: 2 + serialNumber: 132769 + signature: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + validity: + notBefore: Aug 22 10:16:48 2026 GMT + notAfter: Aug 17 10:16:48 2046 GMT + subject: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=CMS Test Signer Alice, emailAddress=alice@example.invalid + key: X509_PUBKEY: + algor: + algorithm: rsaEncryption (1.2.840.113549.1.1.1) + parameter: NULL + public_key: (0 unused bits) + 0000 - 30 82 01 0a 02 82 01 01-00 c6 32 7b 59 a4 0.........2{Y. + 000e - 98 14 ff 12 83 e6 e7 49-df b3 b5 e6 ee 2b .......I.....+ + 001c - c1 7b 33 31 67 3d e1 f4-c9 52 7b b5 6f 6e .{31g=...R{.on + 002a - 40 a9 7b 34 ea 75 ea 0b-04 a2 81 c5 20 7e @.{4.u...... ~ + 0038 - 6b 95 61 cf d7 01 38 05-5a 29 d7 38 30 47 k.a...8.Z).80G + 0046 - 6e ca 2b af d2 5a 25 fd-d1 96 5a 8a 34 0e n.+..Z%...Z.4. + 0054 - e2 61 35 de 72 2c e4 8c-5e c9 7d da 46 86 .a5.r,..^.}.F. + 0062 - 94 9f 8a f6 07 02 61 cf-e4 59 8c 53 10 5a ......a..Y.S.Z + 0070 - a2 33 b6 05 06 27 94 54-1a a8 0f 9b 80 a1 .3...'.T...... + 007e - 43 94 a2 56 a9 fc 92 ba-42 48 bb a3 df db C..V....BH.... + 008c - 14 10 cc c0 ef 40 21 65-0e c4 0c e3 b6 f7 .....@!e...... + 009a - 53 4b d0 fe 4e 24 26 e1-a7 8c 7c 04 39 64 SK..N$&...|.9d + 00a8 - 52 7b 14 34 29 aa 80 f2-91 58 2f f2 81 b5 R{.4)....X/... + 00b6 - 46 23 f2 e6 d4 67 65 6e-35 56 d3 17 07 21 F#...gen5V...! + 00c4 - 60 aa f2 21 e0 8f ac 9b-2c 67 36 1a e3 72 `..!....,g6..r + 00d2 - c3 f8 05 e1 9a 28 af 0e-f1 cd 47 a8 88 1f .....(....G... + 00e0 - 6e 02 53 ee fd 1a b9 e4-d3 e3 df cb 3c f5 n.S.........<. + 00ee - 61 c7 8b e9 f2 28 f3 26-ed 10 59 d2 4a 26 a....(.&..Y.J& + 00fc - 95 ce 34 52 57 ac f7 50-ba 4f f8 45 b9 02 ..4RW..P.O.E.. + 010a - 03 01 00 01 .... + issuerUID: + subjectUID: + extensions: + object: X509v3 Basic Constraints (2.5.29.19) + critical: TRUE + value: + 0000 - 30 00 0. + + object: X509v3 Key Usage (2.5.29.15) + critical: TRUE + value: + 0000 - 03 02 07 80 .... + + object: X509v3 Extended Key Usage (2.5.29.37) + critical: FALSE + value: + 0000 - 30 0a 06 08 2b 06 01 05-05 07 03 04 0...+....... + + object: X509v3 Subject Key Identifier (2.5.29.14) + critical: FALSE + value: + 0000 - 04 14 d6 93 b0 2f ea ed-46 ed aa 1c 9b ...../..F.... + 000d - 46 eb b9 e0 a1 56 6f 26-61 F....Vo&a + + object: X509v3 Authority Key Identifier (2.5.29.35) + critical: FALSE + value: + 0000 - 30 16 80 14 93 bc 0b 0f-98 7f 2d 80 e1 0.........-.. + 000d - 8c bf 5d f4 53 f9 ed 01-a9 2c cf ..].S....,. + + object: X509v3 Certificate Policies (2.5.29.32) + critical: FALSE + value: + 0000 - 30 0f 30 0d 06 0b 2b 06-01 04 01 83 b2 0.0...+...... + 000d - 03 81 4e 0a ..N. + sig_alg: + algorithm: sha256WithRSAEncryption (1.2.840.113549.1.1.11) + parameter: NULL + signature: (0 unused bits) + 0000 - 66 7f 9e 84 1d bf f2 01-38 a9 52 77 6e e9 15 f.......8.Rwn.. + 000f - 53 fd 12 cd 60 13 6d cc-01 c6 25 e1 88 c5 48 S...`.m...%...H + 001e - 7a 31 5d bb 81 88 98 e9-10 74 71 48 06 63 d3 z1]......tqH.c. + 002d - 14 00 21 d3 c1 db 49 20-aa 87 f8 71 6c a0 9e ..!...I ...ql.. + 003c - 34 23 d7 fa 25 d9 b2 75-6c ce 29 b4 6e 88 ca 4#..%..ul.).n.. + 004b - 26 2b d2 0e 36 df 7e ba-58 11 73 49 83 fa 8d &+..6.~.X.sI... + 005a - ac 31 6e d2 ff 84 ab 4b-72 36 0b e0 8f 2a 13 .1n....Kr6...*. + 0069 - 3f 0c b6 a5 a0 4e 39 a8-d6 11 da 97 e3 bd 79 ?....N9.......y + 0078 - 0e 1d 31 37 43 7f 90 6b-c3 8e 9b 65 b6 77 bb ..17C..k...e.w. + 0087 - 12 3c 09 f5 28 c1 b7 2b-ae 50 70 04 2d c4 81 .<..(..+.Pp.-.. + 0096 - 26 34 cc 57 b1 90 e1 6b-f8 f3 5c 16 eb ba 82 &4.W...k..\.... + 00a5 - 13 e4 0a 0b 7c 06 ab 65-d6 5e e1 50 2c 3c c3 ....|..e.^.P,<. + 00b4 - c2 e7 08 97 82 5f 9f 56-80 0f 77 b2 9c f7 f9 ....._.V..w.... + 00c3 - 14 b9 67 26 59 8e 77 e8-44 37 fe f5 47 c8 0e ..g&Y.w.D7..G.. + 00d2 - 88 35 ac 5a 2b 15 a5 7a-19 48 ca 3d 3c 0a 9e .5.Z+..z.H.=<.. + 00e1 - 58 ef b9 62 fb 0d af d1-2f cd ca 9f 18 0c 67 X..b..../.....g + 00f0 - 7a f3 a5 74 11 32 2d 8e-ef a5 d0 ae 37 cc 39 z..t.2-.....7.9 + 00ff - 7f . + crls: + + signerInfos: + version: 1 + d.issuerAndSerialNumber: + issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + serialNumber: 132786 + digestAlgorithm: + algorithm: sha384 (2.16.840.1.101.3.4.2.2) + parameter: + signedAttrs: + object: contentType (1.2.840.113549.1.9.3) + set: + OBJECT:pkcs7-data (1.2.840.113549.1.7.1) + + object: signingTime (1.2.840.113549.1.9.5) + set: + UTCTIME:Aug 22 10:16:48 2026 GMT + + object: messageDigest (1.2.840.113549.1.9.4) + set: + OCTET STRING: + 0000 - f2 39 08 a5 dc 0d 2a 7b-ed 5d 90 55 74 .9....*{.].Ut + 000d - 8e a5 4f 09 2b 8f 7e c8-20 9d 5e 5c 3d ..O.+.~. .^\= + 001a - 18 21 e8 c1 a2 42 6d 8c-46 01 5e 13 72 .!...Bm.F.^.r + 0027 - d7 65 44 52 d5 9c d1 b9-43 .eDR....C + signatureAlgorithm: + algorithm: ecdsa-with-SHA384 (1.2.840.10045.4.3.3) + parameter: + signature: + 0000 - 30 46 02 21 00 89 9b 5d-3e f9 08 c0 e9 c4 5e 0F.!...]>.....^ + 000f - e7 eb 42 c0 a4 93 cc 56-37 a0 f2 7e e9 f2 3b ..B....V7..~..; + 001e - 01 f4 61 7e eb 97 75 02-21 00 ba 2a 7b 2e 4c ..a~..u.!..*{.L + 002d - 10 3d 88 b1 fe 9c 5d 0b-fc 14 1b c5 9a 92 47 .=....].......G + 003c - 1f 16 74 89 ba ba e4 19-7e 73 8c 84 ..t.....~s.. + unsignedAttrs: + + + version: 1 + d.issuerAndSerialNumber: + issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + serialNumber: 132769 + digestAlgorithm: + algorithm: sha384 (2.16.840.1.101.3.4.2.2) + parameter: + signedAttrs: + object: contentType (1.2.840.113549.1.9.3) + set: + OBJECT:pkcs7-data (1.2.840.113549.1.7.1) + + object: signingTime (1.2.840.113549.1.9.5) + set: + UTCTIME:Aug 22 10:16:48 2026 GMT + + object: messageDigest (1.2.840.113549.1.9.4) + set: + OCTET STRING: + 0000 - f2 39 08 a5 dc 0d 2a 7b-ed 5d 90 55 74 .9....*{.].Ut + 000d - 8e a5 4f 09 2b 8f 7e c8-20 9d 5e 5c 3d ..O.+.~. .^\= + 001a - 18 21 e8 c1 a2 42 6d 8c-46 01 5e 13 72 .!...Bm.F.^.r + 0027 - d7 65 44 52 d5 9c d1 b9-43 .eDR....C + signatureAlgorithm: + algorithm: rsaEncryption (1.2.840.113549.1.1.1) + parameter: NULL + signature: + 0000 - 5c 4b e9 6f e2 dc 10 e5-61 1f 87 5b 8c 14 7c \K.o....a..[..| + 000f - 7b 2c be d1 43 3e 53 4b-d3 a9 b6 b7 79 5a 62 {,..C>SK....yZb + 001e - 37 e2 8b c2 0f 55 f2 53-12 a5 96 3c a0 e4 28 7....U.S...<..( + 002d - 0f 71 f8 79 e6 0c 4c 2f-de f0 de 18 8c d6 64 .q.y..L/......d + 003c - f7 1e 45 92 e9 c7 c9 14-0f c4 e8 b9 e3 cb cc ..E............ + 004b - e0 a6 f7 a4 dd 77 43 1d-a5 49 1b a9 47 9a 2e .....wC..I..G.. + 005a - b5 55 4a a4 40 04 5e e2-2b 9f 9f 7e 17 08 76 .UJ.@.^.+..~..v + 0069 - 04 e2 89 92 56 3d e0 78-0f c4 51 1a 51 23 c2 ....V=.x..Q.Q#. + 0078 - 90 3b 1b 35 0d 52 3b ba-e7 8a fc ee b9 58 dc .;.5.R;......X. + 0087 - a1 7e a8 66 5c f5 f0 dd-6a f3 11 86 6b 3d 1d .~.f\...j...k=. + 0096 - 22 f0 52 25 29 32 2f e3-a6 ad 75 b3 6c f7 83 ".R%)2/...u.l.. + 00a5 - e0 1f 46 94 7e cd 77 1f-8c 9b 67 b0 7c 7e 5c ..F.~.w...g.|~\ + 00b4 - d0 60 c2 b0 1e b9 b4 72-bf 2c 04 49 e4 16 fe .`.....r.,.I... + 00c3 - 1f c7 77 c1 74 57 3c 6d-55 7f 06 8b 98 db 0d ..w.tW +\n=== PKCS#7 certificates + CRL === +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 132769 (0x206a1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + Validity + Not Before: Aug 22 10:16:48 2026 GMT + Not After : Aug 17 10:16:48 2046 GMT + Subject: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=CMS Test Signer Alice/emailAddress=alice@example.invalid + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:c6:32:7b:59:a4:98:14:ff:12:83:e6:e7:49:df: + b3:b5:e6:ee:2b:c1:7b:33:31:67:3d:e1:f4:c9:52: + 7b:b5:6f:6e:40:a9:7b:34:ea:75:ea:0b:04:a2:81: + c5:20:7e:6b:95:61:cf:d7:01:38:05:5a:29:d7:38: + 30:47:6e:ca:2b:af:d2:5a:25:fd:d1:96:5a:8a:34: + 0e:e2:61:35:de:72:2c:e4:8c:5e:c9:7d:da:46:86: + 94:9f:8a:f6:07:02:61:cf:e4:59:8c:53:10:5a:a2: + 33:b6:05:06:27:94:54:1a:a8:0f:9b:80:a1:43:94: + a2:56:a9:fc:92:ba:42:48:bb:a3:df:db:14:10:cc: + c0:ef:40:21:65:0e:c4:0c:e3:b6:f7:53:4b:d0:fe: + 4e:24:26:e1:a7:8c:7c:04:39:64:52:7b:14:34:29: + aa:80:f2:91:58:2f:f2:81:b5:46:23:f2:e6:d4:67: + 65:6e:35:56:d3:17:07:21:60:aa:f2:21:e0:8f:ac: + 9b:2c:67:36:1a:e3:72:c3:f8:05:e1:9a:28:af:0e: + f1:cd:47:a8:88:1f:6e:02:53:ee:fd:1a:b9:e4:d3: + e3:df:cb:3c:f5:61:c7:8b:e9:f2:28:f3:26:ed:10: + 59:d2:4a:26:95:ce:34:52:57:ac:f7:50:ba:4f:f8: + 45:b9 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Key Usage: critical + Digital Signature + X509v3 Extended Key Usage: + E-mail Protection + X509v3 Subject Key Identifier: + D6:93:B0:2F:EA:ED:46:ED:AA:1C:9B:46:EB:B9:E0:A1:56:6F:26:61 + X509v3 Authority Key Identifier: + 93:BC:0B:0F:98:7F:2D:80:E1:8C:BF:5D:F4:53:F9:ED:01:A9:2C:CF + X509v3 Certificate Policies: + Policy: 1.3.6.1.4.1.55555.206.10 + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 66:7f:9e:84:1d:bf:f2:01:38:a9:52:77:6e:e9:15:53:fd:12: + cd:60:13:6d:cc:01:c6:25:e1:88:c5:48:7a:31:5d:bb:81:88: + 98:e9:10:74:71:48:06:63:d3:14:00:21:d3:c1:db:49:20:aa: + 87:f8:71:6c:a0:9e:34:23:d7:fa:25:d9:b2:75:6c:ce:29:b4: + 6e:88:ca:26:2b:d2:0e:36:df:7e:ba:58:11:73:49:83:fa:8d: + ac:31:6e:d2:ff:84:ab:4b:72:36:0b:e0:8f:2a:13:3f:0c:b6: + a5:a0:4e:39:a8:d6:11:da:97:e3:bd:79:0e:1d:31:37:43:7f: + 90:6b:c3:8e:9b:65:b6:77:bb:12:3c:09:f5:28:c1:b7:2b:ae: + 50:70:04:2d:c4:81:26:34:cc:57:b1:90:e1:6b:f8:f3:5c:16: + eb:ba:82:13:e4:0a:0b:7c:06:ab:65:d6:5e:e1:50:2c:3c:c3: + c2:e7:08:97:82:5f:9f:56:80:0f:77:b2:9c:f7:f9:14:b9:67: + 26:59:8e:77:e8:44:37:fe:f5:47:c8:0e:88:35:ac:5a:2b:15: + a5:7a:19:48:ca:3d:3c:0a:9e:58:ef:b9:62:fb:0d:af:d1:2f: + cd:ca:9f:18:0c:67:7a:f3:a5:74:11:32:2d:8e:ef:a5:d0:ae: + 37:cc:39:7f + +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 132786 (0x206b2) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + Validity + Not Before: Aug 22 10:16:48 2026 GMT + Not After : Aug 17 10:16:48 2046 GMT + Subject: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=CMS Test Signer Bob/emailAddress=bob@example.invalid + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:39:1b:70:65:e0:f8:e0:9d:80:d4:2b:57:63:7f: + a1:16:5e:f6:11:1d:4d:b7:6b:8e:53:0b:74:94:0b: + ee:96:2b:7c:7b:2a:02:ab:38:1c:fa:25:39:e7:3b: + 88:ff:11:2e:63:15:71:66:67:a1:20:c4:99:91:44: + 47:20:c6:51:11 + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Key Usage: critical + Digital Signature + X509v3 Extended Key Usage: + E-mail Protection + X509v3 Subject Key Identifier: + 08:36:DE:11:38:00:C0:83:E2:A9:4F:41:13:83:47:12:FC:2C:48:DA + X509v3 Authority Key Identifier: + 93:BC:0B:0F:98:7F:2D:80:E1:8C:BF:5D:F4:53:F9:ED:01:A9:2C:CF + X509v3 Certificate Policies: + Policy: 1.3.6.1.4.1.55555.206.10 + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + a9:48:84:f1:99:00:b8:d3:65:73:d4:6e:78:89:b2:fc:5d:0d: + 3d:19:53:82:84:01:5e:4e:bc:63:f9:33:16:6b:84:78:29:fe: + 48:c1:e4:ff:c2:1c:6b:8c:fb:bf:09:4e:93:5d:42:e0:72:0b: + 3f:ed:68:2b:50:22:a1:91:3a:5f:05:cc:77:7e:25:99:95:5f: + e8:2b:82:24:37:3d:a6:dc:14:1c:28:34:be:18:27:39:2d:02: + 74:8d:20:ef:a4:ab:b0:ef:d8:1b:ea:eb:da:ea:02:e1:03:07: + cd:64:06:b6:8d:23:53:02:6b:a4:9b:9f:2d:52:22:cc:28:cf: + 35:2f:17:5f:97:7f:31:1a:6a:25:1f:97:df:c1:2b:89:79:fa: + 2f:c0:d3:ba:10:0d:f2:84:6c:65:2f:78:16:db:6f:da:84:f9: + f6:6d:af:6a:fa:a3:38:d7:17:4f:ea:25:72:69:fd:48:c5:b3: + e1:c0:1c:c0:7d:2d:68:9a:b6:38:39:74:cd:04:9a:91:61:a7: + fb:d4:cc:42:55:29:dc:5e:47:e9:a1:62:b9:cb:87:b1:b9:28: + f7:75:81:ea:e8:84:08:ef:69:b7:08:99:fc:30:51:15:54:06: + 3f:d8:ef:88:b7:76:9f:22:00:27:e9:c5:c1:51:a6:54:31:70: + 4e:d8:b6:0f + +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 132730 (0x2067a) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + Validity + Not Before: Aug 22 10:16:48 2026 GMT + Not After : Aug 17 10:16:48 2046 GMT + Subject: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test TSA + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:da:20:15:ba:82:09:b2:5c:44:0b:c6:3d:6a:50: + ed:d5:43:45:f2:37:63:7f:02:9c:04:ef:45:55:e7: + 5d:39:6e:88:ad:50:51:ab:2f:c1:82:b5:93:3d:71: + b2:52:84:ff:1a:3d:bd:31:6d:3a:e1:26:8d:00:c0: + a7:f6:15:b5:f4:e7:f5:7e:60:5e:f9:32:cc:e4:90: + 3d:fa:e6:98:63:11:fa:c4:95:f5:94:85:7b:34:67: + 30:86:5c:95:6e:86:24:f2:8c:e3:5a:b7:cd:41:b5: + 31:4e:0f:de:34:cb:65:a3:62:76:94:0c:7f:ec:86: + 4b:65:42:13:a6:a3:bf:22:82:20:6b:37:8c:e0:36: + 1b:51:cc:5b:8c:32:70:a5:39:6d:eb:44:12:3c:cb: + 2b:d2:94:50:11:40:ff:4c:e0:aa:1d:69:46:36:74: + 9d:f5:75:a6:89:a3:ab:40:a8:84:dc:45:4d:47:a2: + 61:65:6a:b5:ac:fe:cf:a4:29:3a:88:ac:5a:b4:24: + 68:e8:d2:26:b2:19:63:17:33:53:db:84:cc:02:fd: + 32:9d:d0:0a:55:72:b1:b9:37:23:2e:84:18:1b:88: + 9e:26:79:e9:c6:ec:25:e9:ed:99:e7:9b:1b:06:b2: + db:7d:e9:02:14:9c:ea:a4:0d:96:5d:a8:21:d2:09: + 0e:e9 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Key Usage: critical + Digital Signature, Non Repudiation + X509v3 Extended Key Usage: critical + Time Stamping + X509v3 Subject Key Identifier: + 8C:6A:86:79:0E:48:98:AA:1D:05:30:4F:49:2A:F5:F8:A5:88:11:17 + X509v3 Authority Key Identifier: + 93:BC:0B:0F:98:7F:2D:80:E1:8C:BF:5D:F4:53:F9:ED:01:A9:2C:CF + X509v3 Certificate Policies: + Policy: 1.3.6.1.4.1.55555.206.20 + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 57:3b:1d:d9:9a:05:46:d8:bd:12:90:59:d0:c0:11:34:4e:d5: + 42:42:67:3f:37:d2:bf:83:63:19:bc:af:db:bd:d3:d3:02:34: + da:81:9e:9c:ea:38:15:c8:a1:c1:3a:ba:84:df:0f:39:24:f9: + 49:22:7a:01:b5:a2:75:28:bb:b6:6a:38:9b:29:cf:9f:af:04: + 98:2b:19:c8:ba:60:57:bb:1d:d8:f9:18:4e:bc:6c:4a:4d:4d: + ce:f3:6c:94:23:06:66:6f:a0:aa:6d:ea:3e:6c:52:e6:5b:3c: + 38:02:10:c4:59:23:2a:89:4c:69:a3:a5:63:aa:41:05:44:9d: + 0c:36:86:e8:02:cb:31:f5:0f:ac:6e:85:10:56:51:d9:3b:8d: + b2:18:8e:45:2f:d8:e6:ec:99:e5:ff:63:02:ee:db:36:82:0b: + fd:9d:b5:81:25:17:29:30:0b:aa:94:49:73:0f:b9:af:54:72: + f8:37:81:35:e4:7a:33:30:e2:b0:cf:1b:bb:57:8c:ab:53:2d: + 24:c2:f4:9a:a6:00:98:92:40:88:0e:c4:44:ff:b7:50:b9:a0: + 62:6a:4b:c1:7b:3b:28:e7:57:71:bd:70:d4:5d:a6:e6:a2:be: + 51:fe:4f:a0:50:1f:08:9b:33:e5:79:64:1f:25:03:20:dd:15: + a4:20:e2:96 + +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 33:71:bb:fb:03:8b:ef:ba:98:76:b9:ef:bc:1d:99:05:9a:af:2a:e5 + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + Validity + Not Before: Aug 22 10:16:48 2026 GMT + Not After : Aug 22 10:16:48 2046 GMT + Subject: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:af:4b:5e:0c:8e:d8:e5:5d:e1:22:61:54:14:1f: + ce:33:34:fc:e8:3d:38:c1:73:ac:b2:65:cf:e9:bd: + 16:c9:9b:23:1c:07:3b:08:5d:19:3c:bc:d6:52:b9: + 52:b6:df:ae:5a:e3:d6:8c:9e:8c:82:62:7d:95:64: + 79:3f:e8:f5:bf:23:2b:16:18:db:3d:b9:ba:27:31: + 0b:84:d2:01:97:70:7d:81:58:67:72:f6:47:e9:cb: + 19:53:e4:69:49:a4:e3:f9:07:a7:55:be:c3:41:c0: + b5:13:03:c7:ac:3d:2f:c4:89:0e:54:76:73:fd:4a: + 89:c3:29:f0:70:17:4d:83:7b:6f:b7:10:10:df:3e: + 16:fd:db:73:8f:9b:46:4f:8b:d9:30:ed:98:9b:3b: + da:22:33:d4:e4:37:fc:e9:ee:0d:a3:2b:0a:33:5d: + 2e:e8:8c:b6:97:46:5a:53:0b:ed:61:ff:35:84:64: + 74:c3:59:70:f0:4b:a5:3f:b0:eb:31:41:8f:d9:ef: + 02:55:c8:ea:31:f7:bc:a0:c8:25:f1:fd:41:0a:c6: + e3:ff:19:58:80:cc:ba:27:5c:ff:b3:41:c4:67:33: + 60:e5:2a:7b:41:59:70:88:1b:34:f4:e6:68:d4:16: + 6b:42:e6:72:7c:af:a6:4b:20:0d:9d:59:8f:6b:e6: + c2:db + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Authority Key Identifier: + 93:BC:0B:0F:98:7F:2D:80:E1:8C:BF:5D:F4:53:F9:ED:01:A9:2C:CF + X509v3 Basic Constraints: critical + CA:TRUE, pathlen:1 + X509v3 Key Usage: critical + Certificate Sign, CRL Sign + X509v3 Subject Key Identifier: + 93:BC:0B:0F:98:7F:2D:80:E1:8C:BF:5D:F4:53:F9:ED:01:A9:2C:CF + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 5d:0b:aa:6b:92:25:b9:c6:bd:4c:06:20:11:12:75:65:f9:21: + b4:f4:21:06:c4:26:26:c2:3e:37:ca:cc:2e:6d:83:b2:15:b8: + 9b:c2:47:d9:a0:47:7a:c4:a9:a7:90:0c:30:2a:0c:70:c5:12: + c2:a6:fa:01:74:b5:91:e4:65:9b:b7:a6:8e:93:db:6a:bb:6b: + 0c:bb:f5:ef:83:3a:eb:ba:24:9a:8f:b6:f1:82:f8:c7:41:2c: + 3d:99:14:96:ae:82:83:be:29:2b:87:5d:96:66:fd:e5:e9:09: + 70:29:eb:2e:a1:01:04:42:7f:85:81:db:cc:18:8f:b6:a9:64: + 3a:dc:3e:97:93:fe:94:36:90:21:8e:27:b6:c7:21:eb:f9:0f: + c4:cf:04:79:84:a2:11:16:8b:16:9c:ae:af:20:c1:0b:08:de: + f6:56:84:5d:61:44:fe:38:1c:24:f4:96:8e:ac:19:93:6c:4f: + a2:c8:d0:a6:6e:3e:a0:1e:9f:f8:1c:e7:74:d8:5b:a9:55:51: + 43:25:74:17:77:5a:56:97:b2:09:84:8f:f4:ed:7b:b4:47:6c: + f7:87:c4:da:3f:5d:16:f3:e8:07:28:7d:ea:09:93:d2:0d:43: + af:8a:9c:e6:b9:da:6d:76:98:12:b5:8a:31:6b:2c:84:f6:79: + 20:4a:08:96 + +Certificate Revocation List (CRL): + Version 2 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=IT, O=File Viewer Test Fixtures, OU=Testing Only, CN=Issue 206 Test Root CA + Last Update: Aug 22 10:16:48 2026 GMT + Next Update: Aug 17 10:16:48 2046 GMT + CRL extensions: + X509v3 Authority Key Identifier: + 93:BC:0B:0F:98:7F:2D:80:E1:8C:BF:5D:F4:53:F9:ED:01:A9:2C:CF + X509v3 CRL Number: + 4096 +No Revoked Certificates. + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 03:7a:1c:e1:8b:4a:7a:d9:5f:72:68:5c:72:3e:da:f4:77:79: + 96:b7:e7:3d:77:1f:9a:41:fa:81:19:04:0d:42:e3:51:ea:9f: + cc:ce:5e:85:ec:2c:d7:da:9f:5c:4e:a9:b5:07:05:e0:c5:89: + b9:e5:ba:97:f4:d4:f9:93:60:66:6e:97:6e:be:e4:5e:c0:2d: + 7b:2c:2b:74:11:cb:a6:db:ce:2d:74:cf:b6:4b:a9:b2:a2:e6: + 4a:c4:1a:9a:c7:d8:2f:d7:09:a5:b4:eb:17:65:66:18:80:7e: + 22:6f:16:28:f8:d1:29:da:d5:f6:6b:6d:ad:1a:d0:8d:59:8d: + 88:93:38:33:73:a0:7e:f2:85:41:98:bb:62:4f:96:7f:01:6b: + 94:57:78:24:13:fd:94:84:35:0d:c9:21:c5:5f:41:ae:0d:b7: + 32:73:02:49:bd:f9:74:10:3a:00:4f:db:76:dd:04:b3:d7:44: + be:8c:14:ae:3a:65:d3:8f:2c:59:a5:d9:6a:c5:05:90:45:05: + 70:e4:25:b1:69:f0:9e:85:a8:f8:57:fc:1b:cd:f6:7a:86:ba: + 6f:4a:18:f9:ef:e9:cb:69:23:93:21:24:d9:df:5d:1b:56:04: + 07:8e:d8:5c:07:2c:0d:99:2e:23:a7:f4:3a:9b:d4:a9:f4:86: + 02:53:f1:1f + +\n=== Timestamp request === +Using configuration from /usr/lib/ssl/openssl.cnf +Version: 1 +Hash Algorithm: sha256 +Message data: + 0000 - 26 d6 a9 e5 14 94 ec b6-b7 80 16 74 0f ba b7 3e &..........t...> + 0010 - ee c8 3e ff e4 cc 96 f0-db 09 2f 4b 28 1e 5f 4c ..>......./K(._L +Policy OID: unspecified +Nonce: 0xCE66F452AC3E8E30 +Certificate required: yes +Extensions: +\n=== Timestamp response === +Using configuration from /usr/lib/ssl/openssl.cnf +Status info: +Status: Granted. +Status description: unspecified +Failure info: unspecified + +TST info: +Version: 1 +Policy OID: 1.3.6.1.4.1.55555.206.1 +Hash Algorithm: sha256 +Message data: + 0000 - 26 d6 a9 e5 14 94 ec b6-b7 80 16 74 0f ba b7 3e &..........t...> + 0010 - ee c8 3e ff e4 cc 96 f0-db 09 2f 4b 28 1e 5f 4c ..>......./K(._L +Serial number: 0x02 +Time stamp: Aug 22 10:16:49.022 2026 GMT +Accuracy: 0x01 seconds, 0x01F4 millis, unspecified micros +Ordering: yes +Nonce: 0xCE66F452AC3E8E30 +TSA: DirName:/C=IT/O=File Viewer Test Fixtures/OU=Testing Only/CN=Issue 206 Test TSA +Extensions: +\n=== Rejected timestamp response === +Using configuration from /usr/lib/ssl/openssl.cnf +Status info: +Status: Rejected. +Status description: Synthetic rejection: unsupported digest algorithm +Failure info: unrecognized or unsupported algorithm identifier + +TST info: +Not included. diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/reports/verification.txt b/packages/renderers/signature/test/fixtures/github-206-contributed/reports/verification.txt new file mode 100644 index 000000000..3e5fea6a2 --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/reports/verification.txt @@ -0,0 +1,12 @@ +[OK] encapsulated PDF CMS verifies and extracts byte-for-byte +[OK] CAdES-BES fixture verifies and extracts byte-for-byte +[OK] detached CMS verifies with the matching original +[OK] detached CMS correctly rejects the altered original +[OK] multi-signer CMS verifies and extracts content +[OK] nested CMS verifies through both layers +[OK] RFC 3161 response verifies against invoice.pdf +[OK] standalone RFC 3161 token verifies against invoice.pdf +[OK] RFC 3161 response correctly rejects altered content +[OK] RFC 5544 embedded and external-content fixtures have matching message imprints + +All expected positive and negative checks passed. diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/scripts/REBUILDING.md b/packages/renderers/signature/test/fixtures/github-206-contributed/scripts/REBUILDING.md new file mode 100644 index 000000000..78c1f9ddb --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/scripts/REBUILDING.md @@ -0,0 +1,8 @@ +# Rebuilding + +All cryptographic objects in this pack were generated with OpenSSL 3.5.x and +`asn1crypto` 1.5.x. No private key is distributed. To regenerate equivalent +fixtures, create a disposable test root, CMS signer certificates, and a TSA +certificate whose only critical EKU is `timeStamping`; then run the command +patterns documented in the repository-level README. Use newly generated keys +and delete them after fixture creation. diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/scripts/build_structured_timestamp_fixtures.py b/packages/renderers/signature/test/fixtures/github-206-contributed/scripts/build_structured_timestamp_fixtures.py new file mode 100644 index 000000000..f0fb5b78a --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/scripts/build_structured_timestamp_fixtures.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +from pathlib import Path +from asn1crypto import cms, core, crl, pem, tsp + +ROOT = Path(__file__).resolve().parents[1] + +class MetaData(core.Sequence): + _fields = [ + ('hash_protected', core.Boolean), + ('file_name', core.UTF8String, {'optional': True}), + ('media_type', core.IA5String, {'optional': True}), + ('other_meta_data', cms.CMSAttributes, {'optional': True}), + ] + +class TimeStampAndCRL(core.Sequence): + _fields = [ + ('time_stamp', cms.ContentInfo), + ('crl', crl.CertificateList, {'optional': True}), + ] + +class TimeStampTokenEvidence(core.SequenceOf): + _child_spec = TimeStampAndCRL + +class Evidence(core.Choice): + _alternatives = [ + ('tst_evidence', TimeStampTokenEvidence, {'implicit': 0}), + ('ers_evidence', core.Any, {'implicit': 1}), + ('other_evidence', core.Any, {'implicit': 2}), + ] + +class TimeStampedData(core.Sequence): + _fields = [ + ('version', core.Integer), + ('data_uri', core.IA5String, {'optional': True}), + ('meta_data', MetaData, {'optional': True}), + ('content', core.OctetString, {'optional': True}), + ('temporal_evidence', Evidence), + ] + +OID = '1.2.840.113549.1.9.16.1.31' +cms.ContentType._map[OID] = 'time_stamped_data' +cms.ContentInfo._oid_specs['time_stamped_data'] = TimeStampedData + +def der_from_pem(path: Path) -> bytes: + raw = path.read_bytes() + if pem.detect(raw): + _, _, raw = pem.unarmor(raw) + return raw + +content = (ROOT / 'originals/invoice.pdf').read_bytes() +token = cms.ContentInfo.load((ROOT / 'timestamps/invoice-sha256.tst').read_bytes()) +root_crl = crl.CertificateList.load(der_from_pem(ROOT / 'certificates/test-root-ca.crl.pem')) +evidence = Evidence(name='tst_evidence', value=TimeStampTokenEvidence([ + TimeStampAndCRL({'time_stamp': token, 'crl': root_crl}) +])) +metadata = MetaData({ + 'hash_protected': False, + 'file_name': 'invoice.pdf', + 'media_type': 'application/pdf', +}) + +embedded = TimeStampedData({ + 'version': 1, + 'meta_data': metadata, + 'content': content, + 'temporal_evidence': evidence, +}) +embedded_ci = cms.ContentInfo({'content_type': 'time_stamped_data', 'content': embedded}) +(ROOT / 'timestamps/invoice-embedded.tsd').write_bytes(embedded_ci.dump()) + +external = TimeStampedData({ + 'version': 1, + 'data_uri': 'https://example.invalid/file-viewer-issue-206/invoice.pdf', + 'meta_data': metadata, + 'temporal_evidence': evidence, +}) +external_ci = cms.ContentInfo({'content_type': 'time_stamped_data', 'content': external}) +(ROOT / 'timestamps/invoice-external-content.tsd').write_bytes(external_ci.dump()) + +class FlexibleTimeStampResp(core.Sequence): + _fields = [ + ('status', tsp.PKIStatusInfo), + ('time_stamp_token', cms.ContentInfo, {'optional': True}), + ] + +rejected = FlexibleTimeStampResp({ + 'status': { + 'status': 'rejection', + 'status_string': ['Synthetic rejection: unsupported digest algorithm'], + 'fail_info': {'bad_alg'}, + } +}) +(ROOT / 'timestamps/rejected-bad-alg.tsr').write_bytes(rejected.dump()) diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/scripts/verify_all.sh b/packages/renderers/signature/test/fixtures/github-206-contributed/scripts/verify_all.sh new file mode 100644 index 000000000..87d9a7814 --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/scripts/verify_all.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +ok() { printf '[OK] %s\n' "$1"; } + +openssl cms -verify -binary -inform DER \ + -in "$ROOT/cms/invoice-encapsulated.pdf.p7m" \ + -CAfile "$ROOT/certificates/test-root-ca.pem" -purpose any \ + -out "$TMP/invoice.pdf" >/dev/null 2>&1 +cmp "$TMP/invoice.pdf" "$ROOT/originals/invoice.pdf" +ok 'encapsulated PDF CMS verifies and extracts byte-for-byte' + +openssl cms -verify -cades -binary -inform DER \ + -in "$ROOT/cms/invoice-cades-bes.pdf.p7m" \ + -CAfile "$ROOT/certificates/test-root-ca.pem" -purpose any \ + -out "$TMP/invoice-cades.pdf" >/dev/null 2>&1 +cmp "$TMP/invoice-cades.pdf" "$ROOT/originals/invoice.pdf" +ok 'CAdES-BES fixture verifies and extracts byte-for-byte' + +openssl cms -verify -binary -inform DER \ + -in "$ROOT/cms/invoice-detached.pdf.p7s" \ + -content "$ROOT/originals/invoice.pdf" \ + -CAfile "$ROOT/certificates/test-root-ca.pem" -purpose any \ + -out /dev/null >/dev/null 2>&1 +ok 'detached CMS verifies with the matching original' + +if openssl cms -verify -binary -inform DER \ + -in "$ROOT/cms/invoice-detached.pdf.p7s" \ + -content "$ROOT/originals/invoice-tampered.pdf" \ + -CAfile "$ROOT/certificates/test-root-ca.pem" -purpose any \ + -out /dev/null >/dev/null 2>&1; then + echo '[FAIL] detached CMS unexpectedly verified against altered content' >&2 + exit 1 +fi +ok 'detached CMS correctly rejects the altered original' + +openssl cms -verify -binary -inform DER \ + -in "$ROOT/cms/hello-multi-signer.p7m" \ + -CAfile "$ROOT/certificates/test-root-ca.pem" -purpose any \ + -out "$TMP/hello.txt" >/dev/null 2>&1 +cmp "$TMP/hello.txt" "$ROOT/originals/hello.txt" +ok 'multi-signer CMS verifies and extracts content' + +openssl cms -verify -binary -inform DER \ + -in "$ROOT/cms/invoice-double-signed.p7m" \ + -CAfile "$ROOT/certificates/test-root-ca.pem" -purpose any \ + -out "$TMP/inner.p7m" >/dev/null 2>&1 +openssl cms -verify -binary -inform DER \ + -in "$TMP/inner.p7m" \ + -CAfile "$ROOT/certificates/test-root-ca.pem" -purpose any \ + -out "$TMP/nested-invoice.pdf" >/dev/null 2>&1 +cmp "$TMP/nested-invoice.pdf" "$ROOT/originals/invoice.pdf" +ok 'nested CMS verifies through both layers' + +openssl ts -verify -data "$ROOT/originals/invoice.pdf" \ + -in "$ROOT/timestamps/invoice-sha256.tsr" \ + -CAfile "$ROOT/certificates/test-root-ca.pem" \ + -untrusted "$ROOT/certificates/test-tsa.pem" >/dev/null 2>&1 +ok 'RFC 3161 response verifies against invoice.pdf' + +openssl ts -verify -token_in -data "$ROOT/originals/invoice.pdf" \ + -in "$ROOT/timestamps/invoice-sha256.tst" \ + -CAfile "$ROOT/certificates/test-root-ca.pem" \ + -untrusted "$ROOT/certificates/test-tsa.pem" >/dev/null 2>&1 +ok 'standalone RFC 3161 token verifies against invoice.pdf' + +if openssl ts -verify -data "$ROOT/originals/invoice-tampered.pdf" \ + -in "$ROOT/timestamps/invoice-sha256.tsr" \ + -CAfile "$ROOT/certificates/test-root-ca.pem" \ + -untrusted "$ROOT/certificates/test-tsa.pem" >/dev/null 2>&1; then + echo '[FAIL] timestamp unexpectedly verified against altered content' >&2 + exit 1 +fi +ok 'RFC 3161 response correctly rejects altered content' + +python "$ROOT/scripts/verify_tsd.py" >/dev/null +ok 'RFC 5544 embedded and external-content fixtures have matching message imprints' + +printf '\nAll expected positive and negative checks passed.\n' diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/scripts/verify_tsd.py b/packages/renderers/signature/test/fixtures/github-206-contributed/scripts/verify_tsd.py new file mode 100644 index 000000000..12ca72504 --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/scripts/verify_tsd.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +import hashlib +from pathlib import Path +from asn1crypto import cms, core, tsp + +ROOT = Path(__file__).resolve().parents[1] + +class MetaData(core.Sequence): + _fields = [ + ('hash_protected', core.Boolean), + ('file_name', core.UTF8String, {'optional': True}), + ('media_type', core.IA5String, {'optional': True}), + ('other_meta_data', cms.CMSAttributes, {'optional': True}), + ] +class TimeStampAndCRL(core.Sequence): + _fields = [('time_stamp', cms.ContentInfo), ('crl', core.Any, {'optional': True})] +class TimeStampTokenEvidence(core.SequenceOf): + _child_spec = TimeStampAndCRL +class Evidence(core.Choice): + _alternatives = [ + ('tst_evidence', TimeStampTokenEvidence, {'implicit': 0}), + ('ers_evidence', core.Any, {'implicit': 1}), + ('other_evidence', core.Any, {'implicit': 2}), + ] +class TimeStampedData(core.Sequence): + _fields = [ + ('version', core.Integer), + ('data_uri', core.IA5String, {'optional': True}), + ('meta_data', MetaData, {'optional': True}), + ('content', core.OctetString, {'optional': True}), + ('temporal_evidence', Evidence), + ] +OID = '1.2.840.113549.1.9.16.1.31' +cms.ContentType._map[OID] = 'time_stamped_data' +cms.ContentInfo._oid_specs['time_stamped_data'] = TimeStampedData + +original = (ROOT / 'originals/invoice.pdf').read_bytes() +for name in ('invoice-embedded.tsd', 'invoice-external-content.tsd'): + ci = cms.ContentInfo.load((ROOT / 'timestamps' / name).read_bytes()) + tsd = ci['content'] + assert tsd['version'].native == 1 + assert tsd['meta_data']['hash_protected'].native is False + data = tsd['content'].native if tsd['content'].native is not None else original + token = tsd['temporal_evidence'].chosen[0]['time_stamp'] + signed_data = token['content'] + tst_info = signed_data['encap_content_info']['content'].parsed + imprint = tst_info['message_imprint'] + algorithm = imprint['hash_algorithm']['algorithm'].native + expected = imprint['hashed_message'].native + actual = hashlib.new(algorithm, data).digest() + assert actual == expected, f'{name}: message imprint mismatch' +print('TSD checks passed') diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/hello-sha384-no-nonce.tsq b/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/hello-sha384-no-nonce.tsq new file mode 100644 index 0000000000000000000000000000000000000000..5da02b462ead2918cd925829a6ac24a7ebbdac59 GIT binary patch literal 75 zcmV-R0JQ%wNdf@@FhMX41_@w>NC9O71OfsD00c1dIS8fP4Jv!>U656Dj-^itE02E2 hAe~-ZJs2VA!J^;4*8Z;F=evG@P3i$S7!m+% zQU`&knYH(23ef<7sXHf(SU(O=^kF&V!+ZTaa(ZNa?ktNy~d&-CyU{cYHK1*fn`GaHu zY)E;j2w0Ke*boIPDo7Y+Ly*7#DpkiOR_(ARwVC-lP2aP!d1K_A0OuhuZQREgCc!D^ zot)3h*5YeBw7RpL#MUitdR(U9XKNUdKO0$cu<+cvvz!Muo7We3G5xE{KXnyp zn5L9_UR2#X-mO+8=Pje}I>_mS7xeP)2)l>qa>6|yn`D+7?R8�N>K1{401)Lz6n=zYrPKtZ`7xCr zlYuZn2%`y)7sLsDt^);8L>iGy^-qo#-H=nJfCr9tnzM7oDPl4}k)Ni5$O7s8l9>%8 zN=}0I{wJwd7&ET((@^Q^E+ETafFfdmPxBH;C70eCR=n7~K>}OXDDJmsRTo6As;}`k z&eiBTRf*(i$+g$*+FGyBH!thZ5V&#k=G$)rvs4Is_HoOCCd~s~Mz^muhL^Q^sAwMC z96=!2)fcgfdpO+DwTt=ckkqC_C)w{);8RC;kncdRd^2HPd%B+-W_+;C)#3s_KJ#@wCtc6uTGst z;&emDLgHB4e8b+YpXiWtedcFZG(0bO#{KeDdQ-Io?s~bIZtFo&x2W)ijR&u4yb6)o zoEW>WcG%Nkk-7}$ASQiSIG0kULoZSF1q8(J&`o(no<6f6w7*ly6>k z6HlP~_bftFM$nITg)DBuSm*)X^0zkZdpAPbW7N^xV}#qo$FFgy@lk*^w&e4$E{)X- zSUrIC2f&g?&=*O&!tO^$Jf7B3quN}MG_6U55>7pCLRMF_CZ3@IRvCf@nAFI>&bU4& zz+Kqmxz6Wi?}@A|dkIo-?IrCd6g;p1tRxUr6!0Tu{jL{dtp~!!9@~bSr_#G3LK7$fi{&kr<)27j9HkPh)y0k^5NlpjMsc$J5PJ(-8`y$pHu0|4im-(nf#1j z#LqLSQ3nXTNkucccOw<*qB6pvoz^&;_KF*auDl8?(e^oGG;^`?+*=f%-Ttttv#&l} zWFz&|Sp8J*1($VY8y0R76)y$%GC%HUDtc}qq}Ly!wxw~qq{mr5*-u*a7aIym^U1vd zac6TJn(lGqZv;G!OxouCAp2UCd75){aEgNZQ9YYXBg4SGndrDvd2Lokk1#GRR2Rup uP6;@W|Mpf-;c8`tmOdGLiKMufp?dsKtZusEE!hT#g82PUU-Tu%ll}vA!9LFb literal 0 HcmV?d00001 diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/hello-sha384-no-nonce.tst b/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/hello-sha384-no-nonce.tst new file mode 100644 index 0000000000000000000000000000000000000000..27a556b77fbf04a004f7ad384df8ea4fa90a52d4 GIT binary patch literal 3023 zcmc(hdo+|=AICkYxA*=%dq3a(`+WBgaPS>ioObet ziffxYeFNq%RG#*FvJm8>)00+L#L%|RXqhM@|@GrmW z0pSJu1pLp=q70P2oQ3p{v$*Zs7lGK&z)E?ECEA#J%S#vuq@tR9mde!o2gw3Bkm^zq z1Vw`4KtvJ2xH!y#Advw?u8xDN+F^@tvj}vVzGq|i#>6)P&O^Vn^Bkj_g`}K!aXqKs z%&E?_2(5DCE-trdqz0Qm(KsT~bmpC0^~~68{H~qz2A$6`EIy3p_)2|fJvy6}6l>%U zl6M>pNmhT8I_zD0i&jl_B0e(eU=;3-$|58@Pj+Nj%ajyeQRN^^lhnt?imsj8Qw>*BAIO0;|hEbrorur<8kN zRNp(^tx=`mD{tsN$nAs|4D#=Ydxrg+Z=$BhB`c1S2Cagq}-3KD?|*_nWy0SJN{Rgw|ZEgA+QKvEbHg%Mg%N=<+yj48sH0K~Ck z0-7)wQIy!{I#7~ClhCrMfyvR58*<8&Fu>VCdv>lkMM@qh3)9rlWI)<4o7sRT<|OFu zf0BBIKI1+=4VA9$0%YL=1T6)GG%tZvcImwdidS%NkjU0GO8qA=_I-`g8D4jqq>P;I zSzG&CnRH)C^i^o69BjOM7d9GTpUj&ym1{rUY+lD5BAThsJE+D$)di8O>T3c`bG5oo zRiblr6x!={ZLL@8nZNy{L&(d}zZ;bpB}YT5@k1r!$h zmeDY_`2^$SakzszODqpbTDHZtf6b99a+;lf-{|w>Ba6Q9i$P-!ej&e_O_A3E__8dA za3a6bBW%uu{nN7;L-6$XF2(w-sps5v)mvsxl!)Z~wk}1o)poJgh0og^=-3>)X4Tl% z&_-4dy=1iDvg;>jo{O?ruHi_upE3o7Jd`U!o@NUWZxbM1@da^N!_fLaQHNjF(_--{r72d=5Q%|OHcE=s{i?$k{y`rW=A`e>d>olo&ZZ~v{ z702G@ANFqjM2CX=vjB&p;d#+Bo|muEnyO_`*UK&RTMtTlM1?PGJa|>>6-$0|V(h-! zWy^*vNehGF>ACjKhx@^FPVSlkRl}ZVCLbqWNEGUb8`Aa0Q)`add%NSOeHMS>`bAvz zV!d)Kw)%aFN=xs53469rQr|c5+9|IWXyR?diwfKJch+g{SiC@M^*7u#q8&@~Vx=e= z&5Z8tAu+8-!+xzSaYQ8Yz0#SZErVaU-vj)<52&;d(9gCY2CxGDVF>!x93box6Zo%; zfBOu5CYg>eWBx|rw>56UB2vzZ^+$p~3Oa1t+T z=^UVE?^zg`z_GNZ^iD%)>x3lsU?(}frqs{;f#tExPg%wpDruTE*HxzGiLG7eXl{WN z=`bm;q7rGwi#>X`g-iFb{bagAeI`4tTp=^axuE?{Nr)wH<*B(9V-bDZOFdm}C=v2M zdv7^bh2y5rOvheaAN2k}>wp!V^mbfyRoo;lvs=QZ@=)F=e;_z2N4okDmARVxE*jY= zmQCfrGXj!d3Dso%Kr;U73zqavJPzaumjBf%;80Y76EEqbz>2@;FjV3`lL#f8dfbFwUD29&h6>naXoP82qxd@G`kV-FVUPDZ zzni@$va%dy@FBIAbej z;kX!RQ)zR$x%j}CmARSZhYl&MU&-g|9JcAN- z0L!0LworICQlTj+FCN-yi?VC4xN+#ptDq8HzcVH?7c0-bMKIaz51Tss>cb^AQcjK4 zPxW4KTUWMW;U-S`Qb;f33{{Va_wjoW3t&IXV_>C|6rD8$bv_cG(o<~TLo z9J0cwjq2mk;8 literal 0 HcmV?d00001 diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/invoice-embedded.tsd b/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/invoice-embedded.tsd new file mode 100644 index 0000000000000000000000000000000000000000..0f20bdf8a01ee3614d0ed2c04c2bc4064c2948ec GIT binary patch literal 5820 zcmc&&d011|wkIK=1Q1a~tfGe@%t_A493)@}1Of?T02u;=bPlPFuYKf7a2iY)(`fTt3)?(b5eWp?hd{7%an>3p7_};tW>RZB-IOMiUaL}K zTBCsu1SH$Qos^UqF%_XP$&~uKS<9g`)MPYcQe_bohZ<0`5<}Gx*a4}Hs){nyfI&e< z6Kb#+E6gf1n2bO$WGpHrM?{e6VgzDx8i$VABk34 zP_YpxrN^WC2`C0kgXpmal~JuV6hrhZtwCt8Xx|Ewlw~MTk3jVFiXyDi1Qv4eAFS~& z@CNAWcNK2Lf4PbpZPcs(%6*K#cpvWSf4eWaNUtrg0Cv9h=znvO`4_Kb_q$RAyaxzb zAU+TR7-XngtBf{If^xwchS+>IjlpGeARY^$@pyb5q=4utN;3{B@8yX!^d+zss1Hya z8607tE5&S7rK1>7MaQ#&E=MN;)FfrfVl?{Ix9HWk1X2JDAbOe+1K9(P)8%HRp-&|) zCqu8%N&$!ap~V4PUq=srB{Ql)s4^^QlF{$B6HRVoceLuHwIxMKw7jJn9#W~c$gDJ1 z0z32iQ7kVwxG!QL-%C*yU>^tu)zqf~Co!ZlSDG+mu~}&<0e-!8Nv_58z?bwlF25=R zueU%N;D<)5N2wFE=tN+R)?xvf$$&Y)3xJo&1b9yuEA^KC5aDU^8X}RgqAxw@iCW-% zE`(=d8lF-wvFU@WA2N7OzCJ3}>M?L7$R%OANFw2}V^BbOoWFX2z^et&Bt;FySP8^| z`M4qr24E^9$0;#Ei7Ft28x|-n*A@q+1(v467**k$;+tI_8D1dDij}dW5);SgX_K*} zLWU?qNy!_Zr4mq$Ss7-2sZ7P^6^<2B5mCI5uNyDPSECsiO@j$Tildp~bl zCNCP7t4SzG(*z1ba_ORyxDcOlVqR3NK1CKo6&59>MWYe9@`OO2%#=i$gAu+;&q&$T-z&D*Ok_YBfbb;CI zXmeVDFfcy^jxqZLB?Vc~i1JcFt|&n-k(OlSap^^DRc1Uz8O8T6F*7RE+=$Qwkwz-X z4dzO<5>BaJNuveo(q+XgbCe`dD2dA)ml|#opmLESn;tIEhGj{`k!8hkrL5v`t$w^t zUY@5)r$@=;QeJ{u$o1z4gvk-Aa5am@$xczk>2ix=qi6vE@o=fuFrFVtr!%<8+ERIG z6t7rC73cD`Nr47hWEPLF=f;Y%sA@PSQ)*KAiG0k#i42Vt;Yn16r2K;7@oH&6fp394 zv9Qo5oE4QR4F2VkGGRQEo8coC zP(w@nK8*x@>Q$<}>R6M^sD=zf2uz0Gd>{^+&16Fw=#7j6Dkc7b3~yuz0-D`>G6n*e z@Scpx{P(jg7O02s?c=ci{VW#&HSgVh2+RRQeNTok`t8HaNPB9>B#a@a~XpT~sRDjrwE0;(cEyhA~z^qU@v|MT=fr9x^gDCFD9u5)^Q%`LajDt+w`Qo|s!ld*q!1kuSOr&%f8@L{AJFT_7bAh+aeA=^uO= z%wXie1Ay{rL_!aN@Pg=McSO^Zwjpvh+^Ah$45NrM?Br;lL2?{Ql*kcZ7{V81ml2>& zqEP0``Ygv|CVttnEXXi;D89$p#RZq+!&0(AUx|!_N8n<6mm!kZws^j0OCP!-Av}7xYyUQzGu)L0 zx-P!V1QdnRc!MA@9Kz`WU2yS*jPA1yG1bq$~)?ssM$%}E`Cn!z1eNq(rJ6AQ8 zxRi7^Hsh*-Z zTD8;H2I;tU7dRtR59ih~Ol$aMH)DoG911<&km5e>h4jKYZ>^YvEqhSWG;`JZabMN^ z5-W>asYFX#H#|SOIyijBhMaHxORgOcXc?gz&P?yBIZS-USayt@VgAP!t{<(&YvhdO z0hQPH-J)E-z3A$ak&Z20Q?3&|mfl$HI$!aX&vB1yI@azO~KbLFxhb+Ad-W_0Q-S`dEh;W2%%?$V9@EH7AklJ&0$AjFMI<;a))su_v5EW zf80Rw0^A68S!2D=E_M)~**;`$_OV^=tfZ6C(Zx~R<2$QWBereo+Wzy;MAkX$@})O; z!w!78JG`Ujyc^%YSK?bmXkA$mw6(32we|5pH*=`{gww`D^Mdi45s&mWffjT$g^pkhSCwo_?a z(%Q~%%(--`Zh4)pXv#cwWz@9?JN-gEy}ATjIsy^rn&X58rYY9(#pZ``*AI`MeC(k+y z-(I+6>^WcNiEi%G>vvpNg_4;H+O@j21>&4^=iAB`>>4%peYWXd;+m*$ZX&`tXYUd- z4%|Q3{6CwT z5+T=R*>iMP_H@1Ey#`i)n4m*&g7%e#fv_XsKWKID76*c3zasD-IsR@N>SMCPd7bX> zlr8(VOC2HiAr)w^&$m53j-3Gkdm_0957Wx3TZVnNWKZ1L#<;4^=CyNO;1Czn-XVLM zo)ECsFS^ks?M<`myGpm}T(&;ATOEG0!+-Idw)?in`-NeGMIQ4v+NyU1hkmA*uX%R) zx|1pRChbcCDSGO3Yu90~Ic?4A@LwVq)IV?F%=MWa*tXy2X0Kc4QBq~iN_Wp$o=Z1w zf_6I({O7~W8ig|EId7DI&-~dNM%3$*R~|UlWQc4Wz2fnxtJs;`=8RMk1sgswCv!oI zQ_Y;7+mpYI(?9yG^IVj|^N(u-f~MSXsz2^3+Vr=jSGJrho3L<5>)#MvSk1#qC}H4y z#767^OumOl)ASLU@n$aQH!?Na2pRAvZ${9 zg!q!6)$^XI3)AhVRGmJuj8f&rOxY(}mdMCWWmbqw1)HrfY`xk~J&}%BPs7#|u=OVx z9KHL!rLe- zx%nh@*1%e^oYB2lJi7bfZO4%NU&yp0OSX3$r~cU{4y!FaqjzWD-TK3wFR!`Ij#}UQ zbOT>=>0nKVMSAs-^B16<`cqkzVx0l{Ex(HrcU`U=GDUXkN5Ygfx$C|e8-G%@ z?>C#ry*n$b=Q!}m4!1{_s~zW1w5tS14fgX_n-4zytVuI$eDnD~Lmipx$4;rvk|pdO zd^vV^Z{{TBEq8`@>aKGs+kYwfdMBK?DcJqWql+${%bSl)&JQL;G(88CS2t|^5l{Pf ztHMVF>zxGVy+4{Z!Z7W_RnZx*iXJ2qkvQ1yJx}`Tn|;-&i?b4{&K8(U^30LvdL~xR zZ@(&-IA*c%iFE|YHTqKOjTMKFg4EH22gJ)9;V@7x4y-kpKVy literal 0 HcmV?d00001 diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/invoice-external-content.tsd b/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/invoice-external-content.tsd new file mode 100644 index 0000000000000000000000000000000000000000..8268a235dd7a70868f1a13446db626be1d0da8a7 GIT binary patch literal 3636 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 literal 0 HcmV?d00001 diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/invoice-sha256.tsq b/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/invoice-sha256.tsq new file mode 100644 index 0000000000000000000000000000000000000000..1764bce7f8f4471c4f486a311b19a058909362c6 GIT binary patch literal 70 zcmV-M0J;A#L;?W;FflL<1_@w>NC9O71OfpC00bZ=)~V$bl=$Ugt% c%$D%m2`@`19$!oX2>{M!^ir%ojxYfM|K^$)<^TWy literal 0 HcmV?d00001 diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/invoice-sha256.tsr b/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/invoice-sha256.tsr new file mode 100644 index 0000000000000000000000000000000000000000..7de46f89adadd6b48d915fe08e47a04717992566 GIT binary patch literal 3027 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 literal 0 HcmV?d00001 diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/rejected-bad-alg.tsr b/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/rejected-bad-alg.tsr new file mode 100644 index 000000000..151c82b32 --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/timestamps/rejected-bad-alg.tsr @@ -0,0 +1 @@ +0>0<03 1Synthetic rejection: unsupported digest algorithm \ No newline at end of file diff --git a/packages/renderers/signature/tsconfig.json b/packages/renderers/signature/tsconfig.json new file mode 100644 index 000000000..6cfbfe584 --- /dev/null +++ b/packages/renderers/signature/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2019", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": [ + "DOM", + "WebWorker", + "ES2020" + ], + "strict": true, + "composite": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true, + "verbatimModuleSyntax": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts" + ] +} diff --git a/patches/apply_candidate.py b/patches/apply_candidate.py new file mode 100644 index 000000000..8e0534e83 --- /dev/null +++ b/patches/apply_candidate.py @@ -0,0 +1,45 @@ +from pathlib import Path +import sys + +source = Path(sys.argv[1] if len(sys.argv) > 1 else "source") + +# TypeScript 6 tightened BufferSource typing around ArrayBuffer vs ArrayBufferLike. +# Copy inputs into fresh ArrayBuffers at the WebCrypto boundary. +ts_path = source / "packages/renderers/signature/src/signatureAsn1.ts" +ts = ts_path.read_text() +needle = "const subtleCrypto = () => globalThis.crypto?.subtle;\n" +replacement = needle + "\nconst webCryptoBuffer = (bytes: Uint8Array): ArrayBuffer => {\n const copy = new Uint8Array(bytes.byteLength);\n copy.set(bytes);\n return copy.buffer;\n};\n" +if needle not in ts: + raise SystemExit("signatureAsn1.ts: subtleCrypto anchor not found") +ts = ts.replace(needle, replacement, 1) +ts = ts.replace("subtle.digest(algorithm, bytes)", "subtle.digest(algorithm, webCryptoBuffer(bytes))") +ts = ts.replace("subtle.importKey('spki', certificate.spki, algorithm", "subtle.importKey('spki', webCryptoBuffer(certificate.spki), algorithm") +ts = ts.replace("'spki',\n certificate.spki,", "'spki',\n webCryptoBuffer(certificate.spki),") +ts = ts.replace("subtle.importKey('spki', certificate.spki, { name: 'Ed25519' }", "subtle.importKey('spki', webCryptoBuffer(certificate.spki), { name: 'Ed25519' }") +ts = ts.replace(" signer.signature,\n signedInput", " webCryptoBuffer(signer.signature),\n webCryptoBuffer(signedInput)") +ts = ts.replace(" ecdsaDerToRaw(signer.signature, coordinateLength),\n signedInput", " webCryptoBuffer(ecdsaDerToRaw(signer.signature, coordinateLength)),\n webCryptoBuffer(signedInput)") +ts = ts.replace("subtle.verify('Ed25519', key, signer.signature, signedInput)", "subtle.verify('Ed25519', key, webCryptoBuffer(signer.signature), webCryptoBuffer(signedInput))") +ts = ts.replace("subtle.digest('SHA-256', fullBytes(bytes, certificate.node))", "subtle.digest('SHA-256', webCryptoBuffer(fullBytes(bytes, certificate.node)))") +ts_path.write_text(ts) + +# rPGP 0.20.0 Timestamp is intentionally not Display; use seconds since epoch. +# Literal-data filenames are Bytes and may be non-UTF8, so expose a lossy display string only. +rust_path = source / "packages/renderers/signature/rust/src/inspect.rs" +rust = rust_path.read_text() +rust = rust.replace("subkey.created_at().to_string()", "subkey.created_at().as_secs().to_string()") +rust = rust.replace("key.created_at().to_string()", "key.created_at().as_secs().to_string()") +rust = rust.replace( + ".map(|value| value.file_name().to_owned())\n .filter(|value| !value.is_empty()),", + ".map(|value| String::from_utf8_lossy(value.file_name().as_ref()).into_owned())\n .filter(|value| !value.is_empty()),", +) +rust_path.write_text(rust) + +# The committed CMS fixture embeds CRLF line endings. Make the expected original +# byte-identical so the verification script's byte-for-byte assertion is meaningful. +hello_path = source / "packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello.txt" +hello_path.write_bytes( + b"File Viewer issue 206 synthetic fixture\r\n" + b"This content is signed by two test signers.\r\n" +) + +print("Applied isolated rendition-dss candidate fixes") diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 578300605..a4ebf36bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -779,6 +779,9 @@ importers: '@file-viewer/renderer-presentation': specifier: workspace:2.4.0 version: link:../../renderers/presentation + '@file-viewer/renderer-signature': + specifier: workspace:2.4.0 + version: link:../../renderers/signature '@file-viewer/renderer-spreadsheet': specifier: workspace:2.4.0 version: link:../../renderers/spreadsheet @@ -873,6 +876,9 @@ importers: '@file-viewer/renderer-presentation': specifier: workspace:2.4.0 version: link:../../renderers/presentation + '@file-viewer/renderer-signature': + specifier: workspace:2.4.0 + version: link:../../renderers/signature '@file-viewer/renderer-spreadsheet': specifier: workspace:2.4.0 version: link:../../renderers/spreadsheet @@ -1405,6 +1411,16 @@ importers: specifier: ^6.0.3 version: 6.0.3 + packages/renderers/signature: + dependencies: + '@file-viewer/core': + specifier: workspace:2.4.0 + version: link:../../core + devDependencies: + typescript: + specifier: ^6.0.3 + version: 6.0.3 + packages/renderers/spreadsheet: dependencies: '@file-viewer/core': From d4f9611df2b8c6a82c70b3b2574f7543ef75a6e1 Mon Sep 17 00:00:00 2001 From: p4535992 Date: Thu, 27 Aug 2026 09:54:46 +0200 Subject: [PATCH 2/6] chore:sync --- README.en.md | 10 ++++++ README.md | 10 ++++++ README.zh-CN.md | 10 ++++++ docs/public/_media/sponsors/sponsors.svg | 25 +++++++++++++ patches/apply_candidate.py | 45 ------------------------ 5 files changed, 55 insertions(+), 45 deletions(-) create mode 100644 docs/public/_media/sponsors/sponsors.svg delete mode 100644 patches/apply_candidate.py diff --git a/README.en.md b/README.en.md index 1817cb43b..e0c156fe7 100644 --- a/README.en.md +++ b/README.en.md @@ -193,6 +193,16 @@ If File Viewer saves your team time, [sponsor the maintenance](https://github.co - [WeChat / Alipay](https://dev.flyfish.group/sponsor?source=github): one-time support for domestic users. - [Enterprise support](https://dev.flyfish.group/shop): private deployment, custom file compatibility or work that needs a committed response time. +### Thanks to Our Sponsors + +Thank you for supporting the compatibility work, documentation, and releases that keep File Viewer moving forward. + +

+ + GitHub sponsor @p4535992 + +

+ ## Architecture ```text diff --git a/README.md b/README.md index 1817cb43b..e0c156fe7 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,16 @@ If File Viewer saves your team time, [sponsor the maintenance](https://github.co - [WeChat / Alipay](https://dev.flyfish.group/sponsor?source=github): one-time support for domestic users. - [Enterprise support](https://dev.flyfish.group/shop): private deployment, custom file compatibility or work that needs a committed response time. +### Thanks to Our Sponsors + +Thank you for supporting the compatibility work, documentation, and releases that keep File Viewer moving forward. + +

+ + GitHub sponsor @p4535992 + +

+ ## Architecture ```text diff --git a/README.zh-CN.md b/README.zh-CN.md index 4e18180d9..f749a7bb1 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -699,6 +699,16 @@ Flyfish Viewer 会持续保持 Apache-2.0 开源。GitHub Sponsors 适合一次 - 商业版 Demo: [office.flyfish.dev](https://office.flyfish.dev/) - 飞鱼开源工作室: [flyfish.dev](https://flyfish.dev/) +### 感谢以下赞助者 + +感谢你对 File Viewer 兼容性修复、文档完善和持续发布工作的支持。 + +

+ + GitHub 赞助者 @p4535992 + +

+ | 微信赞赏码 | 支付宝收款码 | 微信公众号二维码 | 用户交流群 | | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | 微信赞赏码 | 支付宝收款码 | 飞鱼开源 WorkShop 微信公众号二维码 | File Viewer 用户交流群二维码 | diff --git a/docs/public/_media/sponsors/sponsors.svg b/docs/public/_media/sponsors/sponsors.svg new file mode 100644 index 000000000..31f8f7125 --- /dev/null +++ b/docs/public/_media/sponsors/sponsors.svg @@ -0,0 +1,25 @@ + + + + + @p4535992 + + + + + + + diff --git a/patches/apply_candidate.py b/patches/apply_candidate.py deleted file mode 100644 index 8e0534e83..000000000 --- a/patches/apply_candidate.py +++ /dev/null @@ -1,45 +0,0 @@ -from pathlib import Path -import sys - -source = Path(sys.argv[1] if len(sys.argv) > 1 else "source") - -# TypeScript 6 tightened BufferSource typing around ArrayBuffer vs ArrayBufferLike. -# Copy inputs into fresh ArrayBuffers at the WebCrypto boundary. -ts_path = source / "packages/renderers/signature/src/signatureAsn1.ts" -ts = ts_path.read_text() -needle = "const subtleCrypto = () => globalThis.crypto?.subtle;\n" -replacement = needle + "\nconst webCryptoBuffer = (bytes: Uint8Array): ArrayBuffer => {\n const copy = new Uint8Array(bytes.byteLength);\n copy.set(bytes);\n return copy.buffer;\n};\n" -if needle not in ts: - raise SystemExit("signatureAsn1.ts: subtleCrypto anchor not found") -ts = ts.replace(needle, replacement, 1) -ts = ts.replace("subtle.digest(algorithm, bytes)", "subtle.digest(algorithm, webCryptoBuffer(bytes))") -ts = ts.replace("subtle.importKey('spki', certificate.spki, algorithm", "subtle.importKey('spki', webCryptoBuffer(certificate.spki), algorithm") -ts = ts.replace("'spki',\n certificate.spki,", "'spki',\n webCryptoBuffer(certificate.spki),") -ts = ts.replace("subtle.importKey('spki', certificate.spki, { name: 'Ed25519' }", "subtle.importKey('spki', webCryptoBuffer(certificate.spki), { name: 'Ed25519' }") -ts = ts.replace(" signer.signature,\n signedInput", " webCryptoBuffer(signer.signature),\n webCryptoBuffer(signedInput)") -ts = ts.replace(" ecdsaDerToRaw(signer.signature, coordinateLength),\n signedInput", " webCryptoBuffer(ecdsaDerToRaw(signer.signature, coordinateLength)),\n webCryptoBuffer(signedInput)") -ts = ts.replace("subtle.verify('Ed25519', key, signer.signature, signedInput)", "subtle.verify('Ed25519', key, webCryptoBuffer(signer.signature), webCryptoBuffer(signedInput))") -ts = ts.replace("subtle.digest('SHA-256', fullBytes(bytes, certificate.node))", "subtle.digest('SHA-256', webCryptoBuffer(fullBytes(bytes, certificate.node)))") -ts_path.write_text(ts) - -# rPGP 0.20.0 Timestamp is intentionally not Display; use seconds since epoch. -# Literal-data filenames are Bytes and may be non-UTF8, so expose a lossy display string only. -rust_path = source / "packages/renderers/signature/rust/src/inspect.rs" -rust = rust_path.read_text() -rust = rust.replace("subkey.created_at().to_string()", "subkey.created_at().as_secs().to_string()") -rust = rust.replace("key.created_at().to_string()", "key.created_at().as_secs().to_string()") -rust = rust.replace( - ".map(|value| value.file_name().to_owned())\n .filter(|value| !value.is_empty()),", - ".map(|value| String::from_utf8_lossy(value.file_name().as_ref()).into_owned())\n .filter(|value| !value.is_empty()),", -) -rust_path.write_text(rust) - -# The committed CMS fixture embeds CRLF line endings. Make the expected original -# byte-identical so the verification script's byte-for-byte assertion is meaningful. -hello_path = source / "packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello.txt" -hello_path.write_bytes( - b"File Viewer issue 206 synthetic fixture\r\n" - b"This content is signed by two test signers.\r\n" -) - -print("Applied isolated rendition-dss candidate fixes") From ad946019d114aa603293fe53c8c36abea07996b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=88=AC=E7=9A=84=E8=80=81=E7=B2=89=E4=B8=9D?= <727842003@qq.com> Date: Thu, 27 Aug 2026 18:33:19 +0800 Subject: [PATCH 3/6] fix(signature): keep renderer opt-in and reproducible --- .../workflows/rendition-dss-validation.yml | 93 +- docs/guide/formats.md | 3 + docs/guide/on-demand-renderers.md | 3 + .../_media/issues/issue-206-signature-cms.png | Bin 0 -> 217034 bytes docs/zh/guide/formats.md | 1 + docs/zh/guide/on-demand-renderers.md | 3 + package.json | 2 +- packages/components/web-full/package.json | 1 - packages/presets/all/package.json | 1 - packages/presets/all/src/index.ts | 3 +- packages/renderers/signature/README.en.md | 2 +- packages/renderers/signature/README.md | 2 +- .../signature/THIRD_PARTY_NOTICES.md | 2 +- packages/renderers/signature/package.json | 6 +- packages/renderers/signature/rust/Cargo.lock | 1683 +++++++++++++++++ .../renderers/signature/rust/src/inspect.rs | 2 +- .../signature/scripts/build-wasm.mjs | 3 +- .../scripts/verify-fixture-checksums.mjs | 36 + .../scripts/verify-optional-boundary.mjs | 147 ++ packages/renderers/signature/src/index.ts | 2 +- .../github-206-contributed/.gitattributes | 2 + .../originals/hello-tampered.txt | 4 +- .../originals/hello.txt | 4 +- pnpm-lock.yaml | 6 - 24 files changed, 1951 insertions(+), 60 deletions(-) create mode 100644 docs/public/_media/issues/issue-206-signature-cms.png create mode 100644 packages/renderers/signature/rust/Cargo.lock create mode 100644 packages/renderers/signature/scripts/verify-fixture-checksums.mjs create mode 100644 packages/renderers/signature/scripts/verify-optional-boundary.mjs create mode 100644 packages/renderers/signature/test/fixtures/github-206-contributed/.gitattributes diff --git a/.github/workflows/rendition-dss-validation.yml b/.github/workflows/rendition-dss-validation.yml index b1699c102..b4e053b7d 100644 --- a/.github/workflows/rendition-dss-validation.yml +++ b/.github/workflows/rendition-dss-validation.yml @@ -1,71 +1,80 @@ name: Rendition DSS validation on: + pull_request: + branches: [main] + paths: + - '.github/workflows/rendition-dss-validation.yml' + - 'package.json' + - 'pnpm-lock.yaml' + - 'packages/components/*-full/package.json' + - 'packages/presets/all/**' + - 'packages/renderers/signature/**' push: branches: [main] + paths: + - '.github/workflows/rendition-dss-validation.yml' + - 'package.json' + - 'pnpm-lock.yaml' + - 'packages/components/*-full/package.json' + - 'packages/presets/all/**' + - 'packages/renderers/signature/**' workflow_dispatch: permissions: - contents: write + contents: read + +concurrency: + group: rendition-dss-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: validate: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 45 steps: - name: Checkout cloneable source - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - persist-credentials: true - - - name: Materialize validated fixes on main when needed - if: github.ref == 'refs/heads/main' - shell: bash - run: | - if grep -q "const webCryptoBuffer" packages/renderers/signature/src/signatureAsn1.ts; then - echo 'Validated fixes are already materialized in main.' - exit 0 - fi - - python3 patches/apply_candidate.py . - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add \ - packages/renderers/signature/src/signatureAsn1.ts \ - packages/renderers/signature/rust/src/inspect.rs \ - packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello.txt - git commit -m 'fix(signature): materialize validated renderer fixes' - git push origin HEAD:main + persist-credentials: false - name: Set up pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 with: version: 11.0.9 run_install: false - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: '22' + node-version: '24.19.0' cache: pnpm cache-dependency-path: pnpm-lock.yaml - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: - python-version: '3.12' + python-version: '3.12.12' - name: Set up Rust shell: bash run: | - rustup toolchain install stable --profile minimal - rustup default stable + rustup toolchain install 1.88.0 --profile minimal + rustup default 1.88.0 rustup target add wasm32-unknown-unknown - cargo install wasm-bindgen-cli --locked + cargo install wasm-bindgen-cli --version 0.2.127 --locked - name: Install Python fixture dependency - run: python -m pip install asn1crypto + shell: bash + run: | + python3 -m venv /tmp/file-viewer-signature-venv + echo '/tmp/file-viewer-signature-venv/bin' >> "$GITHUB_PATH" + /tmp/file-viewer-signature-venv/bin/python -m pip install \ + --disable-pip-version-check \ + --only-binary=:all: \ + --require-hashes \ + 'asn1crypto==1.5.1 --hash=sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67' - name: Toolchain versions shell: bash @@ -91,6 +100,11 @@ jobs: continue-on-error: true run: pnpm --filter @file-viewer/renderer-signature type-check + - name: Verify signature renderer opt-in boundary + id: optional_boundary + continue-on-error: true + run: pnpm --filter @file-viewer/renderer-signature verify:optional-boundary + - name: Issue 206 CMS and timestamp verification id: github206 continue-on-error: true @@ -117,7 +131,7 @@ jobs: shell: bash working-directory: packages/renderers/signature/rust run: | - if [ ! -f Cargo.lock ]; then cargo generate-lockfile; fi + test -s Cargo.lock cargo build --release --locked --target wasm32-unknown-unknown wasm-bindgen target/wasm32-unknown-unknown/release/file_viewer_rpgp_wrapper.wasm \ --target web \ @@ -160,6 +174,7 @@ jobs: env: CORE_BUILD: ${{ steps.core_build.outcome }} SIGNATURE_TYPECHECK: ${{ steps.signature_typecheck.outcome }} + OPTIONAL_BOUNDARY: ${{ steps.optional_boundary.outcome }} GITHUB206: ${{ steps.github206.outcome }} FIXTURES: ${{ steps.fixtures.outcome }} OPENPGP_ARCH: ${{ steps.openpgp_arch.outcome }} @@ -170,6 +185,9 @@ jobs: GOVERNANCE: ${{ steps.governance.outcome }} OFFLINE_ASSETS: ${{ steps.offline_assets.outcome }} OFFLINE_AVAILABLE: ${{ steps.offline_assets.outputs.available }} + VALIDATED_REPOSITORY: ${{ github.repository }} + VALIDATED_REF: ${{ github.ref_name }} + VALIDATED_SHA: ${{ github.sha }} run: | if [ "$OFFLINE_AVAILABLE" = 'true' ]; then OFFLINE_DISPLAY="$OFFLINE_ASSETS" @@ -180,15 +198,17 @@ jobs: { echo '## Rendition DSS validation — cloneable main' echo - echo '- Repository: `p4535992/file-viewer-rendition-dss-ci`' - echo '- Branch: `main`' + echo "- Repository: \`$VALIDATED_REPOSITORY\`" + echo "- Ref: \`$VALIDATED_REF\`" + echo "- Commit: \`$VALIDATED_SHA\`" echo '- Validated fixes are stored directly in the source tree; no runtime patch is required.' - echo '- Runner: `ubuntu-latest`' + echo '- Runner: `ubuntu-24.04`' echo echo '| Check | Outcome |' echo '| --- | --- |' echo "| Core build | $CORE_BUILD |" echo "| Signature type-check | $SIGNATURE_TYPECHECK |" + echo "| Opt-in dependency boundary | $OPTIONAL_BOUNDARY |" echo "| Issue #206 verification | $GITHUB206 |" echo "| Fixture verification | $FIXTURES |" echo "| OpenPGP architecture | $OPENPGP_ARCH |" @@ -204,6 +224,7 @@ jobs: for value in \ "$CORE_BUILD" \ "$SIGNATURE_TYPECHECK" \ + "$OPTIONAL_BOUNDARY" \ "$GITHUB206" \ "$FIXTURES" \ "$OPENPGP_ARCH" \ diff --git a/docs/guide/formats.md b/docs/guide/formats.md index 1eed90714..0c2091157 100644 --- a/docs/guide/formats.md +++ b/docs/guide/formats.md @@ -21,6 +21,7 @@ | Presentations | binary `ppt`, `pot`; OpenXML `pptx`, `pptm`, `potx`, `potm`, `ppsx`, `ppsm`; OpenDocument `odp`; stable high-fidelity `key` | | Apple documents | stable high-fidelity `pages`, `numbers`, and `key` with iWork '09 XML/APXL and modern Snappy/IWA parser paths | | Layout documents | `pdf`, `ofd`, `typ`, `typst` | +| Digital signatures (explicit opt-in, experimental) | `p7m`, `p7s`, `p7b`, `p7c`, `pkcs7`, `cms`, `cmsc`, `tsq`, `tsr`, `tst`, `tsd`, `asc`, `sig`, `pgp`, `gpg` through `@file-viewer/renderer-signature`; not included in `preset-all` or `*-full` packages | | Archives | `zip`, `7z`, `rar`, `tar`, `gz`, `tgz`, `cab`, `iso`, `apk`, `cbz`, `cbr`, and more | | Email | `eml`, `msg`, `mbox` | | Diagrams and mind maps | `xmind`, `drawio`, `dio`, `excalidraw`, `mermaid`, `mmd`, `plantuml`, `puml` | @@ -40,6 +41,8 @@ The full machine-readable matrix, including containers, levels, status, and limits, is generated from `ecosystem/format-catalog.json` into [`docs/generated/format-catalog.md`](/generated/format-catalog). +The signature row is an additional experimental, package-owned definition and is not counted in the 221-extension default catalog above. It must be installed and registered explicitly. Parsing or a valid cryptographic result does not establish certificate trust, policy compliance, qualified-signature status, identity assurance, or legal validity. + ## Engineering Renderer Notes - Word preview uses `@file-viewer/renderer-word`. The package lazy-loads the self-maintained DOCX engine, `msdoc-viewer`, and RTF/OpenDocument helpers only for DOCX/DOC/RTF/ODT files, so core-only and lightweight component installs do not pull Word engines by default. diff --git a/docs/guide/on-demand-renderers.md b/docs/guide/on-demand-renderers.md index 6a66c559b..42f8fca4a 100644 --- a/docs/guide/on-demand-renderers.md +++ b/docs/guide/on-demand-renderers.md @@ -68,6 +68,7 @@ Install a single renderer when a product needs the smallest possible capability | `@file-viewer/renderer-ofd` | `ofdRenderer` | OFD | | `@file-viewer/renderer-cad` | `cadRenderer` | DWG, DXF, DWF, DWFx, XPS | | `@file-viewer/renderer-3d` | `modelRenderer` | 3D models and lightweight geometry signatures | +| `@file-viewer/renderer-signature` | `signatureRenderer` | Explicit opt-in, experimental CMS/CAdES, timestamp, and public OpenPGP inspection; excluded from `preset-all` and `*-full` packages | Binary PowerPoint and OpenXML PowerPoint share the presentation plugin but keep separate lazy engine boundaries. The packaged `.ppt` 0.3.3 runtime is zero-config in standard layouts; for custom asset layouts, configure `presentation.pptModuleUrl` / `pptWorkerUrl` / `pptWasmUrl` / `pptFontUrl`. PPTX continues to use `presentation.workerUrl` / `workerType`. @@ -88,6 +89,8 @@ Strict PPTX-only applications can import `pptxRenderer` from `@file-viewer/rende Engine packages such as `@file-viewer/pptx`, `@file-viewer/geometry-engine`, `@file-viewer/eda-layout`, and `@file-viewer/eda-orcad` are maintained for renderer internals and advanced reuse. Normal viewer integrations should use the renderer or preset package above. +Install `@file-viewer/renderer-signature` directly and pass `signatureRenderer` in `options.renderers`. Its optional Rust/WASM toolchain and cryptographic scope are deliberately not pulled into existing Full installs. + ## Automatic Preset Assembly For an Office document platform, the bundler-neutral path is: diff --git a/docs/public/_media/issues/issue-206-signature-cms.png b/docs/public/_media/issues/issue-206-signature-cms.png new file mode 100644 index 0000000000000000000000000000000000000000..10c370131d838658091f3a983723b166a1d48e82 GIT binary patch literal 217034 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 = { id: 'file-viewer-preset-all', label: 'Flyfish File Viewer full renderer preset', - renderers: [wordRenderer, wordPerfectRenderer, hangulRenderer, iworkRenderer, pdfRenderer, ofdRenderer, presentationRenderer, spreadsheetRenderer, signatureRenderer, cadRenderer, typstRenderer, drawingRenderer, modelRenderer, archiveRenderer, emailRenderer, ebookRenderer, textRenderer, imageRenderer, mediaRenderer, mindmapRenderer, geoRenderer, dataRenderer, edaRenderer, fileViewerAllRendererPlugin], + renderers: [wordRenderer, wordPerfectRenderer, hangulRenderer, iworkRenderer, pdfRenderer, ofdRenderer, presentationRenderer, spreadsheetRenderer, cadRenderer, typstRenderer, drawingRenderer, modelRenderer, archiveRenderer, emailRenderer, ebookRenderer, textRenderer, imageRenderer, mediaRenderer, mindmapRenderer, geoRenderer, dataRenderer, edaRenderer, fileViewerAllRendererPlugin], }; export const fileViewerPresetAll = allRenderers; diff --git a/packages/renderers/signature/README.en.md b/packages/renderers/signature/README.en.md index a2f1f8544..ff83be0c2 100644 --- a/packages/renderers/signature/README.en.md +++ b/packages/renderers/signature/README.en.md @@ -36,7 +36,7 @@ const options = { }; ``` -`@file-viewer/preset-all` includes this renderer explicitly, but `@file-viewer/core` does not import it. +This package is an explicit opt-in. It is not a dependency of `@file-viewer/preset-all`, any `*-full` package, or `@file-viewer/core`. ## OpenPGP backend diff --git a/packages/renderers/signature/README.md b/packages/renderers/signature/README.md index a2f1f8544..ff83be0c2 100644 --- a/packages/renderers/signature/README.md +++ b/packages/renderers/signature/README.md @@ -36,7 +36,7 @@ const options = { }; ``` -`@file-viewer/preset-all` includes this renderer explicitly, but `@file-viewer/core` does not import it. +This package is an explicit opt-in. It is not a dependency of `@file-viewer/preset-all`, any `*-full` package, or `@file-viewer/core`. ## OpenPGP backend diff --git a/packages/renderers/signature/THIRD_PARTY_NOTICES.md b/packages/renderers/signature/THIRD_PARTY_NOTICES.md index 69b2bcb03..a075a18e9 100644 --- a/packages/renderers/signature/THIRD_PARTY_NOTICES.md +++ b/packages/renderers/signature/THIRD_PARTY_NOTICES.md @@ -3,4 +3,4 @@ The optional OpenPGP backend uses **rPGP / `pgp` 0.20.0** when the WASM artifact is built. rPGP is licensed under **MIT OR Apache-2.0**. See . -The exact transitive Rust dependency set is locked by `rust/Cargo.lock` when generated in a Rust-enabled build environment. +The exact transitive Rust dependency set is committed in `rust/Cargo.lock`; builds fail closed when that lockfile is missing. diff --git a/packages/renderers/signature/package.json b/packages/renderers/signature/package.json index 8aa25b931..85a0d709d 100644 --- a/packages/renderers/signature/package.json +++ b/packages/renderers/signature/package.json @@ -64,10 +64,12 @@ "build": "pnpm build:ts && pnpm build:wasm", "type-check": "tsc -b tsconfig.json", "verify:github-206": "pnpm build:ts && node scripts/verify-github-206.mjs", - "verify:fixtures": "bash test/fixtures/github-206-contributed/scripts/verify_all.sh", + "verify:fixtures": "node scripts/verify-fixture-checksums.mjs && bash test/fixtures/github-206-contributed/scripts/verify_all.sh", + "verify:optional-boundary": "node scripts/verify-optional-boundary.mjs", "verify:openpgp-architecture": "node scripts/verify-openpgp-architecture.mjs", "verify:openpgp-detection": "pnpm build:ts && node scripts/verify-openpgp-detection.mjs", - "verify:openpgp": "pnpm verify:openpgp-architecture && pnpm verify:openpgp-detection" + "verify:openpgp": "pnpm verify:openpgp-architecture && pnpm verify:openpgp-detection", + "verify": "pnpm verify:optional-boundary && pnpm verify:fixtures && pnpm verify:github-206 && pnpm verify:openpgp" }, "dependencies": { "@file-viewer/core": "workspace:2.4.0" diff --git a/packages/renderers/signature/rust/Cargo.lock b/packages/renderers/signature/rust/Cargo.lock new file mode 100644 index 000000000..b65b5db3c --- /dev/null +++ b/packages/renderers/signature/rust/Cargo.lock @@ -0,0 +1,1683 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "bytes", + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-kw" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c" +dependencies = [ + "aes", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", + "zeroize", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitfields" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6e59298da389bc0649c7463856b34c6e17fe542f88939426ede4436c6b1195" +dependencies = [ + "bitfields-impl", +] + +[[package]] +name = "bitfields-impl" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c044f98f86f15414668d6c8187c7e4fadab1ad2b31680f648703e0fe07c555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "thiserror", +] + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "buffer-redux" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "431a9cc8d7efa49bc326729264537f5e60affce816c66edf434350778c9f4f54" +dependencies = [ + "memchr", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "camellia" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3264e2574e9ef2b53ce6f536dea83a69ac0bc600b762d1523ff83fe07230ce30" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "cast5" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911" +dependencies = [ + "cipher", +] + +[[package]] +name = "cfb-mode" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330" +dependencies = [ + "cipher", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cmac" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" +dependencies = [ + "cipher", + "dbl", + "digest", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc24" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "cx448" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c0cf476284b03eb6c10e78787b21c7abb7d7d43cb2f02532ba6b831ed892fa" +dependencies = [ + "crypto-bigint", + "elliptic-curve", + "pkcs8", + "rand_core", + "serdect 0.3.0", + "sha3", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dbl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" +dependencies = [ + "generic-array", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.119", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dsa" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" +dependencies = [ + "digest", + "num-bigint-dig", + "num-traits", + "pkcs8", + "rfc6979", + "sha2", + "signature", + "zeroize", +] + +[[package]] +name = "eax" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" +dependencies = [ + "aead", + "cipher", + "cmac", + "ctr", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "base64ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "serde_json", + "serdect 0.2.0", + "subtle", + "tap", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "file-viewer-rpgp-wrapper" +version = "0.1.0" +dependencies = [ + "pgp", + "serde", + "serde-wasm-bindgen", + "wasm-bindgen", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "idea" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075557004419d7f2031b8bb7f44bb43e55a83ca7b63076a8fb8fe75753836477" +dependencies = [ + "cipher", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ocb3" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c196e0276c471c843dd5777e7543a36a298a4be942a2a688d8111cd43390dedb" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core", + "sha2", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pgp" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfa4743b28656065ff4c0ba09e46b357a65e8c00fc2341e89084b82f87cbdf1" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "aes-kw", + "argon2", + "base64", + "bitfields", + "block-padding", + "blowfish", + "buffer-redux", + "byteorder", + "bytes", + "camellia", + "cast5", + "cfb-mode", + "cipher", + "const-oid", + "crc24", + "curve25519-dalek", + "cx448", + "derive_builder", + "derive_more", + "des", + "digest", + "dsa", + "eax", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "flate2", + "generic-array", + "getrandom", + "hex", + "hkdf", + "idea", + "k256", + "log", + "md-5", + "memchr", + "nom", + "num-bigint-dig", + "num-traits", + "num_enum", + "ocb3", + "p256", + "p384", + "p521", + "rand", + "replace_with", + "ripemd", + "rsa", + "sha1", + "sha1-checked", + "sha2", + "sha3", + "signature", + "smallvec", + "snafu", + "subtle", + "twofish", + "web-time", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "replace_with" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect 0.2.0", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "serdect" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1-checked" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" +dependencies = [ + "digest", + "sha1", + "zeroize", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "snafu" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45cb604038abb7b926b679887b3226d8d0f23874b66623625a0454be425a4b7" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287f59010008f0d7cf5e3b03196d666c1acc46c8d3e9cf34c28a1a7157601e72" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "twofish" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013" +dependencies = [ + "cipher", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/packages/renderers/signature/rust/src/inspect.rs b/packages/renderers/signature/rust/src/inspect.rs index fb47652c4..76662d359 100644 --- a/packages/renderers/signature/rust/src/inspect.rs +++ b/packages/renderers/signature/rust/src/inspect.rs @@ -89,7 +89,7 @@ pub fn inspect(input: &[u8], limits: &ParseLimits, include_literal: bool) -> Res }); } - if let Ok((mut message, _headers)) = Message::from_reader(Cursor::new(input.to_vec())) { + if let Ok((message, _headers)) = Message::from_reader(Cursor::new(input.to_vec())) { let encrypted = message.is_encrypted(); let compressed = message.is_compressed(); let signed = message.is_signed() || message.is_one_pass_signed(); diff --git a/packages/renderers/signature/scripts/build-wasm.mjs b/packages/renderers/signature/scripts/build-wasm.mjs index 6411e4c8f..93719b30a 100644 --- a/packages/renderers/signature/scripts/build-wasm.mjs +++ b/packages/renderers/signature/scripts/build-wasm.mjs @@ -26,8 +26,7 @@ await mkdir(outDir, { recursive: true }); try { await access(resolve(rustDir, 'Cargo.lock')); } catch { - console.warn('[renderer-signature] Cargo.lock is missing; generating it before the locked build.'); - run('cargo', ['generate-lockfile']); + throw new Error('[renderer-signature] rust/Cargo.lock is required for a reproducible WASM build.'); } run('cargo', ['build', '--release', '--locked', '--target', 'wasm32-unknown-unknown']); run('wasm-bindgen', [wasmInput, '--target', 'web', '--out-dir', outDir, '--out-name', 'rpgp_wrapper']); diff --git a/packages/renderers/signature/scripts/verify-fixture-checksums.mjs b/packages/renderers/signature/scripts/verify-fixture-checksums.mjs new file mode 100644 index 000000000..212976c87 --- /dev/null +++ b/packages/renderers/signature/scripts/verify-fixture-checksums.mjs @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { dirname, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; + +const fixturesRoot = resolve( + dirname(fileURLToPath(import.meta.url)), + "../test/fixtures/github-206-contributed", +); +const manifest = await readFile(resolve(fixturesRoot, "SHA256SUMS"), "utf8"); +const entries = manifest.split(/\r?\n/u).filter(Boolean); + +assert( + entries.length > 0, + "The contributed fixture checksum manifest must not be empty.", +); + +for (const line of entries) { + const match = line.match(/^([a-f0-9]{64}) \.\/(.+)$/u); + assert(match, `Invalid SHA256SUMS entry: ${line}`); + const [, expected, relativePath] = match; + const absolutePath = resolve(fixturesRoot, relativePath); + assert( + absolutePath.startsWith(`${fixturesRoot}${sep}`), + `Fixture checksum path escapes the corpus root: ${relativePath}`, + ); + const actual = createHash("sha256") + .update(await readFile(absolutePath)) + .digest("hex"); + assert.equal(actual, expected, `Fixture checksum mismatch: ${relativePath}`); +} + +console.log( + `Verified ${entries.length} immutable Issue #206 fixture checksums.`, +); diff --git a/packages/renderers/signature/scripts/verify-optional-boundary.mjs b/packages/renderers/signature/scripts/verify-optional-boundary.mjs new file mode 100644 index 000000000..06bc304b4 --- /dev/null +++ b/packages/renderers/signature/scripts/verify-optional-boundary.mjs @@ -0,0 +1,147 @@ +import assert from "node:assert/strict"; +import { readdir, readFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const repositoryRoot = resolve(packageDir, "../../.."); +const read = (path) => readFile(resolve(repositoryRoot, path), "utf8"); +const readJson = async (path) => JSON.parse(await read(path)); + +const readWorkspacePackages = async () => { + const packagesRoot = resolve(repositoryRoot, "packages"); + const packages = []; + for (const entry of await readdir(packagesRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const directManifest = resolve(packagesRoot, entry.name, "package.json"); + try { + packages.push(JSON.parse(await readFile(directManifest, "utf8"))); + continue; + } catch (error) { + if (error?.code !== "ENOENT") throw error; + } + for (const child of await readdir(resolve(packagesRoot, entry.name), { + withFileTypes: true, + })) { + if (!child.isDirectory()) continue; + const manifest = resolve( + packagesRoot, + entry.name, + child.name, + "package.json", + ); + try { + packages.push(JSON.parse(await readFile(manifest, "utf8"))); + } catch (error) { + if (error?.code !== "ENOENT") throw error; + } + } + } + return packages; +}; + +const dependencyNames = (manifest) => + ["dependencies", "optionalDependencies"].flatMap((section) => + Object.keys(manifest[section] || {}), + ); + +const [ + presetPackage, + presetSource, + rootPackage, + rendererSource, + rendererReadme, + formatsGuide, + workspacePackages, +] = await Promise.all([ + readJson("packages/presets/all/package.json"), + read("packages/presets/all/src/index.ts"), + readJson("package.json"), + read("packages/renderers/signature/src/index.ts"), + read("packages/renderers/signature/README.en.md"), + read("docs/guide/formats.md"), + readWorkspacePackages(), +]); + +const packageByName = new Map( + workspacePackages.map((manifest) => [manifest.name, manifest]), +); +const fullPackages = workspacePackages + .filter((manifest) => manifest.name?.endsWith("-full")) + .sort((left, right) => left.name.localeCompare(right.name)); +const expectedFullPackages = [ + "@file-viewer/jquery-full", + "@file-viewer/react-full", + "@file-viewer/react-legacy-full", + "@file-viewer/svelte-full", + "@file-viewer/vue2.6-full", + "@file-viewer/vue2.7-full", + "@file-viewer/vue3-full", + "@file-viewer/web-full", +]; + +assert.deepEqual( + fullPackages.map((manifest) => manifest.name), + expectedFullPackages, + "The opt-in boundary must cover every published Full package.", +); + +for (const fullPackage of fullPackages) { + const queue = [fullPackage.name]; + const visited = new Set(); + while (queue.length > 0) { + const currentName = queue.shift(); + if (visited.has(currentName)) continue; + visited.add(currentName); + assert.notEqual( + currentName, + "@file-viewer/renderer-signature", + `${fullPackage.name} must not pull renderer-signature through its runtime dependency closure.`, + ); + const manifest = packageByName.get(currentName); + if (!manifest) continue; + queue.push( + ...dependencyNames(manifest).filter((name) => packageByName.has(name)), + ); + } +} + +assert( + !presetPackage.dependencies?.["@file-viewer/renderer-signature"], + "preset-all must not depend on renderer-signature.", +); +assert( + rootPackage.scripts?.["build:renderers"]?.includes( + "!@file-viewer/renderer-signature", + ), + "The default renderer build must leave the optional Rust/WASM package out of the standard path.", +); +assert( + !presetSource.includes("@file-viewer/renderer-signature"), + "preset-all must not import renderer-signature.", +); +assert( + !presetSource.includes("signatureRenderer"), + "preset-all must not register signatureRenderer.", +); +assert.match( + rendererSource, + /presets:\s*\[\s*\]/, + "renderer-signature must declare no default preset membership.", +); +assert( + rendererReadme.includes("explicit opt-in"), + "renderer README must describe the package as explicit opt-in.", +); +assert( + formatsGuide.includes("Digital signatures (explicit opt-in, experimental)"), + "format guide must list the optional signature pipeline.", +); +assert( + formatsGuide.includes("not included in `preset-all` or `*-full` packages"), + "format guide must state the default Full boundary.", +); + +console.log( + "Verified renderer-signature remains an explicit opt-in outside preset-all and Full package dependencies.", +); diff --git a/packages/renderers/signature/src/index.ts b/packages/renderers/signature/src/index.ts index 9fd912b5f..d3745a145 100644 --- a/packages/renderers/signature/src/index.ts +++ b/packages/renderers/signature/src/index.ts @@ -14,7 +14,7 @@ export const signatureRendererDefinition: RendererDefinition = { supportLevel: 'experimental', status: 'experimental', packageName: '@file-viewer/renderer-signature', - presets: ['all'], + presets: [], knownLimits: [ 'Phase one supports CMS/PKCS#7, selected CAdES attributes, RFC 3161, and RFC 5544 TimeStampedData.', 'Certificate trust, policy compliance, qualified status, and legal validity are not established.', diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/.gitattributes b/packages/renderers/signature/test/fixtures/github-206-contributed/.gitattributes new file mode 100644 index 000000000..0c58b96c2 --- /dev/null +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/.gitattributes @@ -0,0 +1,2 @@ +* -text +originals/hello*.txt whitespace=cr-at-eol diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello-tampered.txt b/packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello-tampered.txt index 697274184..5198139c4 100644 --- a/packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello-tampered.txt +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello-tampered.txt @@ -1,2 +1,2 @@ -File Viewer issue 206 synthetic fixture -This content was intentionally changed. +File Viewer issue 206 synthetic fixture +This content was intentionally changed. diff --git a/packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello.txt b/packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello.txt index 5afafeb1a..5bd506b83 100644 --- a/packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello.txt +++ b/packages/renderers/signature/test/fixtures/github-206-contributed/originals/hello.txt @@ -1,2 +1,2 @@ -File Viewer issue 206 synthetic fixture -This content is signed by two test signers. +File Viewer issue 206 synthetic fixture +This content is signed by two test signers. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4ebf36bf..c21aed42f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -779,9 +779,6 @@ importers: '@file-viewer/renderer-presentation': specifier: workspace:2.4.0 version: link:../../renderers/presentation - '@file-viewer/renderer-signature': - specifier: workspace:2.4.0 - version: link:../../renderers/signature '@file-viewer/renderer-spreadsheet': specifier: workspace:2.4.0 version: link:../../renderers/spreadsheet @@ -876,9 +873,6 @@ importers: '@file-viewer/renderer-presentation': specifier: workspace:2.4.0 version: link:../../renderers/presentation - '@file-viewer/renderer-signature': - specifier: workspace:2.4.0 - version: link:../../renderers/signature '@file-viewer/renderer-spreadsheet': specifier: workspace:2.4.0 version: link:../../renderers/spreadsheet From 0b4a456d937e8c1027617dcc65dcbc6e63b51c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=88=AC=E7=9A=84=E8=80=81=E7=B2=89=E4=B8=9D?= <727842003@qq.com> Date: Thu, 27 Aug 2026 18:46:15 +0800 Subject: [PATCH 4/6] test(signature): verify browser security boundaries --- .../workflows/rendition-dss-validation.yml | 11 + packages/renderers/signature/package.json | 3 +- .../signature/scripts/verify-browser.mjs | 203 ++++++++++++++++++ 3 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 packages/renderers/signature/scripts/verify-browser.mjs diff --git a/.github/workflows/rendition-dss-validation.yml b/.github/workflows/rendition-dss-validation.yml index b4e053b7d..ddbd21cef 100644 --- a/.github/workflows/rendition-dss-validation.yml +++ b/.github/workflows/rendition-dss-validation.yml @@ -145,6 +145,14 @@ jobs: continue-on-error: true run: pnpm --filter @file-viewer/renderer-signature build + - name: Install pinned Playwright browsers + run: npm exec --yes --package=playwright@1.61.1 -- playwright install --with-deps chromium firefox webkit + + - name: Browser rendering, CSP, offline, and cleanup verification + id: signature_browser + continue-on-error: true + run: pnpm --filter @file-viewer/renderer-signature verify:browser + - name: Full repository type-check id: repo_typecheck continue-on-error: true @@ -181,6 +189,7 @@ jobs: OPENPGP: ${{ steps.openpgp.outcome }} RUST_WASM: ${{ steps.rust_wasm.outcome }} SIGNATURE_BUILD: ${{ steps.signature_build.outcome }} + SIGNATURE_BROWSER: ${{ steps.signature_browser.outcome }} REPO_TYPECHECK: ${{ steps.repo_typecheck.outcome }} GOVERNANCE: ${{ steps.governance.outcome }} OFFLINE_ASSETS: ${{ steps.offline_assets.outcome }} @@ -215,6 +224,7 @@ jobs: echo "| OpenPGP full verification | $OPENPGP |" echo "| Standalone Rust/WASM | $RUST_WASM |" echo "| Signature package build | $SIGNATURE_BUILD |" + echo "| Browser rendering / CSP / offline / cleanup | $SIGNATURE_BROWSER |" echo "| Repository type-check | $REPO_TYPECHECK |" echo "| Governance | $GOVERNANCE |" echo "| Offline assets | $OFFLINE_DISPLAY |" @@ -231,6 +241,7 @@ jobs: "$OPENPGP" \ "$RUST_WASM" \ "$SIGNATURE_BUILD" \ + "$SIGNATURE_BROWSER" \ "$REPO_TYPECHECK" \ "$GOVERNANCE"; do if [ "$value" != 'success' ]; then diff --git a/packages/renderers/signature/package.json b/packages/renderers/signature/package.json index 85a0d709d..e51cad417 100644 --- a/packages/renderers/signature/package.json +++ b/packages/renderers/signature/package.json @@ -65,11 +65,12 @@ "type-check": "tsc -b tsconfig.json", "verify:github-206": "pnpm build:ts && node scripts/verify-github-206.mjs", "verify:fixtures": "node scripts/verify-fixture-checksums.mjs && bash test/fixtures/github-206-contributed/scripts/verify_all.sh", + "verify:browser": "pnpm build:ts && npm exec --yes --package=playwright@1.61.1 -- node scripts/verify-browser.mjs", "verify:optional-boundary": "node scripts/verify-optional-boundary.mjs", "verify:openpgp-architecture": "node scripts/verify-openpgp-architecture.mjs", "verify:openpgp-detection": "pnpm build:ts && node scripts/verify-openpgp-detection.mjs", "verify:openpgp": "pnpm verify:openpgp-architecture && pnpm verify:openpgp-detection", - "verify": "pnpm verify:optional-boundary && pnpm verify:fixtures && pnpm verify:github-206 && pnpm verify:openpgp" + "verify": "pnpm verify:optional-boundary && pnpm verify:fixtures && pnpm verify:github-206 && pnpm verify:openpgp && pnpm verify:browser" }, "dependencies": { "@file-viewer/core": "workspace:2.4.0" diff --git a/packages/renderers/signature/scripts/verify-browser.mjs b/packages/renderers/signature/scripts/verify-browser.mjs new file mode 100644 index 000000000..63abf5eee --- /dev/null +++ b/packages/renderers/signature/scripts/verify-browser.mjs @@ -0,0 +1,203 @@ +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import { delimiter, dirname, resolve, sep } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const repositoryRoot = resolve(packageRoot, "../../.."); +const require = createRequire(import.meta.url); + +const importPlaywright = async () => { + for (const binDir of (process.env.PATH || "") + .split(delimiter) + .filter((entry) => entry.endsWith(`${sep}node_modules${sep}.bin`))) { + try { + const entry = require.resolve("playwright", { + paths: [resolve(binDir, "..")], + }); + return await import(pathToFileURL(entry).href); + } catch { + // npm exec exposes its temporary package root through PATH. + } + } + return import("playwright"); +}; + +const startServer = async () => { + const appRequire = createRequire( + resolve(repositoryRoot, "apps/viewer-demo/package.json"), + ); + const vite = await import(pathToFileURL(appRequire.resolve("vite")).href); + const server = await vite.createServer({ + root: packageRoot, + logLevel: "error", + server: { + host: "127.0.0.1", + port: 0, + hmr: false, + fs: { allow: [repositoryRoot, packageRoot] }, + headers: { + "Content-Security-Policy": [ + "default-src 'self'", + "script-src 'self' 'wasm-unsafe-eval'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: blob:", + "connect-src 'self'", + "worker-src 'self' blob:", + "object-src 'none'", + "base-uri 'none'", + "require-trusted-types-for 'script'", + "trusted-types file-viewer-test", + ].join("; "), + }, + }, + }); + await server.listen(); + const address = server.httpServer?.address(); + assert( + address && typeof address !== "string", + "Unable to resolve the browser test URL.", + ); + return { server, origin: `http://127.0.0.1:${address.port}` }; +}; + +const verifyBrowser = async (browserType, browserName, origin) => { + const browser = await browserType.launch({ headless: true }); + try { + const page = await browser.newPage({ + viewport: { width: 1440, height: 1024 }, + }); + const failures = []; + const externalRequests = []; + page.on("console", (message) => { + if (message.type() === "error") + failures.push(`console: ${message.text()}`); + }); + page.on("pageerror", (error) => + failures.push(`pageerror: ${error.message}`), + ); + page.on("request", (request) => { + const url = request.url(); + if ( + !url.startsWith(origin) && + !url.startsWith("data:") && + !url.startsWith("blob:") + ) { + externalRequests.push(url); + } + }); + + await page.goto(`${origin}/README.md`, { waitUntil: "domcontentloaded" }); + const state = await page.evaluate(async () => { + document.head.replaceChildren(); + const target = document.createElement("div"); + target.id = "signature-browser-test"; + target.style.width = "100vw"; + target.style.height = "100vh"; + document.body.style.margin = "0"; + document.body.replaceChildren(target); + + const [{ default: renderSignature }, response] = await Promise.all([ + import("/dist/signature.js"), + fetch( + "/test/fixtures/github-206-contributed/cms/invoice-encapsulated.pdf.p7m", + ), + ]); + if (!response.ok) + throw new Error(`Fixture returned HTTP ${response.status}`); + const rendered = await renderSignature( + await response.arrayBuffer(), + target, + "p7m", + { + filename: "invoice-encapsulated.pdf.p7m", + options: {}, + renderNestedBuffer: async (_buffer, extension, nestedTarget) => { + nestedTarget.textContent = `Nested ${extension.toUpperCase()} preview verified`; + return { destroy() {} }; + }, + }, + ); + const snapshot = { + text: target.innerText, + signerCount: target.querySelectorAll(".signature-item").length, + validStatusCount: target.querySelectorAll( + ".signature-status[data-state='valid']", + ).length, + errorCount: target.querySelectorAll(".signature-error").length, + eventAttributeCount: target.querySelectorAll( + "[onabort],[onerror],[onload],[onclick],[onmouseover]", + ).length, + }; + await rendered.unmount?.(); + return { ...snapshot, remainingChildren: target.childElementCount }; + }); + + assert.match( + state.text, + /CMS \/ PKCS#7 SignedData/u, + `[${browserName}] CMS was not rendered.`, + ); + assert.match( + state.text, + /Signature: valid/u, + `[${browserName}] signature was not verified.`, + ); + assert.match( + state.text, + /Content digest: valid/u, + `[${browserName}] digest was not verified.`, + ); + assert( + state.signerCount >= 1, + `[${browserName}] signer metadata is missing.`, + ); + assert( + state.validStatusCount >= 2, + `[${browserName}] expected two valid status badges.`, + ); + assert.equal( + state.errorCount, + 0, + `[${browserName}] renderer reported an error.`, + ); + assert.equal( + state.eventAttributeCount, + 0, + `[${browserName}] unsafe event attributes reached the DOM.`, + ); + assert.equal( + state.remainingChildren, + 0, + `[${browserName}] unmount left rendered DOM behind.`, + ); + assert.deepEqual( + externalRequests, + [], + `[${browserName}] renderer attempted an external request.`, + ); + assert.deepEqual( + failures, + [], + `[${browserName}] browser failures: ${failures.join("; ")}`, + ); + console.log( + `[signature-browser] ${browserName}: CMS render, verification, CSP, offline and cleanup passed.`, + ); + } finally { + await browser.close(); + } +}; + +const playwrightModule = await importPlaywright(); +const { chromium, firefox, webkit } = playwrightModule.chromium + ? playwrightModule + : playwrightModule.default; +const { server, origin } = await startServer(); +try { + await verifyBrowser(chromium, "chromium", origin); + await verifyBrowser(firefox, "firefox", origin); + await verifyBrowser(webkit, "webkit", origin); +} finally { + await server.close(); +} From 3574a07de4b70e8b37e23d503dfcd7af9b97cb26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=88=AC=E7=9A=84=E8=80=81=E7=B2=89=E4=B8=9D?= <727842003@qq.com> Date: Thu, 27 Aug 2026 18:56:45 +0800 Subject: [PATCH 5/6] ci(signature): install hashed fixture dependency --- .../workflows/rendition-dss-validation.yml | 30 +++++++++---------- .../signature/test/requirements-fixtures.txt | 1 + 2 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 packages/renderers/signature/test/requirements-fixtures.txt diff --git a/.github/workflows/rendition-dss-validation.yml b/.github/workflows/rendition-dss-validation.yml index ddbd21cef..7f82c02b0 100644 --- a/.github/workflows/rendition-dss-validation.yml +++ b/.github/workflows/rendition-dss-validation.yml @@ -4,21 +4,21 @@ on: pull_request: branches: [main] paths: - - '.github/workflows/rendition-dss-validation.yml' - - 'package.json' - - 'pnpm-lock.yaml' - - 'packages/components/*-full/package.json' - - 'packages/presets/all/**' - - 'packages/renderers/signature/**' + - ".github/workflows/rendition-dss-validation.yml" + - "package.json" + - "pnpm-lock.yaml" + - "packages/components/*-full/package.json" + - "packages/presets/all/**" + - "packages/renderers/signature/**" push: branches: [main] paths: - - '.github/workflows/rendition-dss-validation.yml' - - 'package.json' - - 'pnpm-lock.yaml' - - 'packages/components/*-full/package.json' - - 'packages/presets/all/**' - - 'packages/renderers/signature/**' + - ".github/workflows/rendition-dss-validation.yml" + - "package.json" + - "pnpm-lock.yaml" + - "packages/components/*-full/package.json" + - "packages/presets/all/**" + - "packages/renderers/signature/**" workflow_dispatch: permissions: @@ -48,14 +48,14 @@ jobs: - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: '24.19.0' + node-version: "24.19.0" cache: pnpm cache-dependency-path: pnpm-lock.yaml - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: - python-version: '3.12.12' + python-version: "3.12.12" - name: Set up Rust shell: bash @@ -74,7 +74,7 @@ jobs: --disable-pip-version-check \ --only-binary=:all: \ --require-hashes \ - 'asn1crypto==1.5.1 --hash=sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67' + --requirement packages/renderers/signature/test/requirements-fixtures.txt - name: Toolchain versions shell: bash diff --git a/packages/renderers/signature/test/requirements-fixtures.txt b/packages/renderers/signature/test/requirements-fixtures.txt new file mode 100644 index 000000000..40cdc4cf1 --- /dev/null +++ b/packages/renderers/signature/test/requirements-fixtures.txt @@ -0,0 +1 @@ +asn1crypto==1.5.1 --hash=sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67 From f2d6b00b8834ae3f82b88710517fe9c1b2307768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=88=AC=E7=9A=84=E8=80=81=E7=B2=89=E4=B8=9D?= <727842003@qq.com> Date: Thu, 27 Aug 2026 18:58:37 +0800 Subject: [PATCH 6/6] style(ci): apply governance formatting --- .../workflows/rendition-dss-validation.yml | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/rendition-dss-validation.yml b/.github/workflows/rendition-dss-validation.yml index 7f82c02b0..71d6a43b6 100644 --- a/.github/workflows/rendition-dss-validation.yml +++ b/.github/workflows/rendition-dss-validation.yml @@ -4,21 +4,21 @@ on: pull_request: branches: [main] paths: - - ".github/workflows/rendition-dss-validation.yml" - - "package.json" - - "pnpm-lock.yaml" - - "packages/components/*-full/package.json" - - "packages/presets/all/**" - - "packages/renderers/signature/**" + - '.github/workflows/rendition-dss-validation.yml' + - 'package.json' + - 'pnpm-lock.yaml' + - 'packages/components/*-full/package.json' + - 'packages/presets/all/**' + - 'packages/renderers/signature/**' push: branches: [main] paths: - - ".github/workflows/rendition-dss-validation.yml" - - "package.json" - - "pnpm-lock.yaml" - - "packages/components/*-full/package.json" - - "packages/presets/all/**" - - "packages/renderers/signature/**" + - '.github/workflows/rendition-dss-validation.yml' + - 'package.json' + - 'pnpm-lock.yaml' + - 'packages/components/*-full/package.json' + - 'packages/presets/all/**' + - 'packages/renderers/signature/**' workflow_dispatch: permissions: @@ -48,14 +48,14 @@ jobs: - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "24.19.0" + node-version: '24.19.0' cache: pnpm cache-dependency-path: pnpm-lock.yaml - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: - python-version: "3.12.12" + python-version: '3.12.12' - name: Set up Rust shell: bash