diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 16a464a85..f53cf1424 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,13 @@ { "image": "mcr.microsoft.com/devcontainers/dotnet:9.0", + "features": { + "ghcr.io/devcontainers/features/dotnet:2": { + "version": "none", + "additionalVersions": "8.0,10.0" + } + }, + "customizations": { "vscode": { "extensions": [ diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b2974e49d..0c938aece 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -24,11 +24,19 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "monthly" + interval: "weekly" day: "wednesday" time: "09:00" timezone: "Europe/Stockholm" groups: github-actions: patterns: - - "*" \ No newline at end of file + - "*" + + - package-ecosystem: docker + directory: / + schedule: + interval: "weekly" + day: "wednesday" + time: "09:00" + timezone: "Europe/Stockholm" diff --git a/.github/workflows/build-nativeshims.yml b/.github/workflows/build-nativeshims.yml index 8970eba2c..428b83f69 100644 --- a/.github/workflows/build-nativeshims.yml +++ b/.github/workflows/build-nativeshims.yml @@ -29,12 +29,20 @@ on: schedule: - cron: '0 0 * * *' # Every day at midnight +permissions: + contents: read + jobs: build-windows: name: Build Windows runs-on: windows-2022 steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - run: | @@ -52,25 +60,25 @@ jobs: } else { & ./build-windows.ps1 } - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: win-x64 path: Yubico.NativeShims/win-x64/** - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: win-x86 path: Yubico.NativeShims/win-x86/** - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: win-arm64 path: Yubico.NativeShims/win-arm64/** - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: nuspec path: | Yubico.NativeShims/*.nuspec Yubico.NativeShims/readme.md - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: msbuild path: Yubico.NativeShims/msbuild/* @@ -79,7 +87,12 @@ jobs: name: Build Linux (amd64) runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Install Zig (pinned version) @@ -222,7 +235,7 @@ jobs: readelf -V *.so | grep GLIBC_2 | sort -u echo "✅ Binary compatible with Debian 10 (glibc 2.28)" ' - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: linux-x64 path: Yubico.NativeShims/linux-x64/*.so @@ -231,7 +244,12 @@ jobs: name: Build Linux (arm64) runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Install Zig (pinned version) @@ -304,7 +322,7 @@ jobs: bash ./build-linux-arm64.sh fi - name: Set up QEMU for ARM64 testing - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 with: platforms: arm64 - name: Test on Ubuntu 18.04 (glibc 2.27) @@ -378,7 +396,7 @@ jobs: readelf -V *.so | grep GLIBC_2 | sort -u echo "✅ ARM64 binary compatible with Debian 10 (glibc 2.28)" ' - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: linux-arm64 path: Yubico.NativeShims/linux-arm64/*.so @@ -387,7 +405,12 @@ jobs: name: Build macOS runs-on: macos-14 steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - run: | @@ -399,11 +422,11 @@ jobs: else sh ./build-macOS.sh fi - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: osx-x64 path: Yubico.NativeShims/osx-x64/** - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: osx-arm64 path: Yubico.NativeShims/osx-arm64/** @@ -421,8 +444,13 @@ jobs: PACKAGE_VERSION: ${{ github.event.inputs.version != '' && github.event.inputs.version || '1.0.0' }} GITHUB_REPO_URL: https://github.com/${{ github.repository }} steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + - name: Download contents, set metadata and package - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - run: | mv nuspec/*.nuspec . mv nuspec/readme.md . @@ -437,13 +465,13 @@ jobs: - run: nuget pack Yubico.NativeShims.nuspec - name: Upload Nuget Package - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: NuGet Package NativeShims path: Yubico.NativeShims.*.nupkg - name: Generate artifact attestation - uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: subject-path: | Yubico.NativeShims/**/*.dll @@ -460,7 +488,12 @@ jobs: packages: write if: ${{ github.event.inputs.push-to-dev == 'true' }} steps: - - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: NuGet Package NativeShims - run: | diff --git a/.github/workflows/build-pull-requests.yml b/.github/workflows/build-pull-requests.yml index 52ab19b9b..48df12369 100644 --- a/.github/workflows/build-pull-requests.yml +++ b/.github/workflows/build-pull-requests.yml @@ -29,14 +29,17 @@ on: - '.github/workflows/build-pull-requests.yml' permissions: - pull-requests: write - checks: write contents: read - packages: read - + jobs: run-tests: name: Run tests + # Requires write permissions to publish test results and coverage reports to PR + permissions: + pull-requests: write # Required to comment on PRs with test results + checks: write # Required to create check runs for test results + contents: read + packages: read uses: ./.github/workflows/test.yml with: build-coverage-report: true @@ -47,10 +50,15 @@ jobs: needs: run-tests steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: global-json-file: global.json source-url: https://nuget.pkg.github.com/Yubico/index.json @@ -63,7 +71,7 @@ jobs: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Save build artifacts - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Nuget Packages Release path: | @@ -71,7 +79,7 @@ jobs: Yubico.YubiKey/src/bin/Release/*.nupkg - name: Save build artifacts - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Assemblies Release path: | diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b08e17032..100e3870a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,14 +48,18 @@ on: schedule: - cron: '0 0 * * *' # Every day at midnight +permissions: + contents: read + jobs: run-tests: name: Run tests + # Requires write permissions to publish test results permissions: - checks: write + checks: write # Required to create check runs for test results + pull-requests: write # Required to comment on PRs with test results contents: read packages: read - pull-requests: write uses: ./.github/workflows/test.yml with: build-coverage-report: false @@ -64,11 +68,12 @@ jobs: name: Build artifacts runs-on: windows-2022 needs: run-tests + # Requires write permissions to generate artifact attestations permissions: - id-token: write + id-token: write # Required for OIDC token generation + attestations: write # Required to attest build provenance contents: read packages: read - attestations: write outputs: docs-log-id: ${{ steps.docs-log-upload.outputs.artifact-id }} docs-id: ${{ steps.docs-upload.outputs.artifact-id }} @@ -76,10 +81,15 @@ jobs: symbols-packages-id: ${{ steps.symbols-upload.outputs.artifact-id }} assemblies-id: ${{ steps.assemblies-upload.outputs.artifact-id }} steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: global-json-file: "./global.json" source-url: https://nuget.pkg.github.com/Yubico/index.json @@ -109,7 +119,7 @@ jobs: # Upload documentation log - name: "Save build artifacts: Docs log" id: docs-log-upload - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Documentation log path: docfx.log @@ -118,7 +128,7 @@ jobs: # Upload documentation - name: "Save build artifacts: Docs" id: docs-upload - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Documentation path: docs/_site/ @@ -127,7 +137,7 @@ jobs: # Upload NuGet packages - name: "Save build artifacts: Nuget Packages" id: nuget-upload - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Nuget Packages path: | @@ -138,7 +148,7 @@ jobs: # Upload symbols - name: "Save build artifacts: Symbols Packages" id: symbols-upload - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Symbols Packages path: | @@ -149,7 +159,7 @@ jobs: # Upload assemblies - name: "Save build artifacts: Assemblies" id: assemblies-upload - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Assemblies path: | @@ -159,7 +169,7 @@ jobs: # Generate artifact attestation - name: Generate artifact attestation - uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: subject-path: | Yubico.Core/src/bin/Release/*.nupkg @@ -172,8 +182,9 @@ jobs: upload-docs: name: Upload docs if: ${{ github.event.inputs.push-to-docs == 'true' }} + # Requires write permission for OIDC authentication to GCP permissions: - id-token: write + id-token: write # Required for OIDC token generation contents: read uses: ./.github/workflows/upload-docs.yml needs: build-artifacts @@ -183,14 +194,20 @@ jobs: runs-on: windows-2022 needs: build-artifacts if: ${{ github.event.inputs.push-to-dev == 'true' }} + # Requires write permission to publish NuGet packages permissions: + packages: write # Required to publish to GitHub Packages contents: read - packages: write steps: - - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: Nuget Packages - - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: source-url: https://nuget.pkg.github.com/Yubico/index.json env: @@ -209,6 +226,11 @@ jobs: needs: [run-tests, build-artifacts, publish-internal, upload-docs] if: always() steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + - name: Generate build summary env: # Pass job results into the environment diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 64e5fbe76..d0930eb32 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -10,6 +10,9 @@ on: pull_request_review: types: [submitted] +permissions: + contents: read + jobs: claude: if: | @@ -18,22 +21,28 @@ jobs: (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) runs-on: ubuntu-latest + # Requires write permissions for Claude Code to interact with repository permissions: - contents: write - pull-requests: write - issues: write - id-token: write - actions: read # Required for Claude to read CI results on PRs + contents: write # Required for Claude to commit/push changes + pull-requests: write # Required to comment on and manage PRs + issues: write # Required to comment on and manage issues + id-token: write # Required for OIDC token generation + actions: read # Required for Claude to read CI results on PRs steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 persist-credentials: false - name: Run Claude Code id: claude - uses: anthropics/claude-code-action@426380f01bad0a17200865605a85cb28926dccbf # v1.0.9 + uses: anthropics/claude-code-action@2817c54db8f44f3a0485d57292566bf07d428a25 # v1.0.37 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2661b92f3..ab28ec6ee 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -40,28 +40,33 @@ on: - '.github/workflows/*.yml' permissions: - # required for all workflows - security-events: write - - # only required for workflows in private repositories - actions: read contents: read - packages: read jobs: analyze: name: Analyze runs-on: windows-2022 + # Requires write permission to upload CodeQL security scan results + permissions: + security-events: write # Required for CodeQL to upload scan results + actions: read # Required for workflows in private repositories + contents: read + packages: read steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # Setup .NET with authenticated NuGet source - name: Setup .NET - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: source-url: https://nuget.pkg.github.com/Yubico/index.json env: @@ -69,7 +74,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 + uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 with: # Override automatic language detection to only analyze C# # C/C++ code in Yubico.NativeShims is built separately (requires CMake/vcpkg) @@ -82,4 +87,4 @@ jobs: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 + uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 000000000..690337a46 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,27 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, +# PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - name: 'Checkout Repository' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: 'Dependency Review' + uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 945b6e855..f607d7906 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -26,8 +26,13 @@ jobs: runs-on: ubuntu-latest steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + - name: Check out current repo - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: path: self persist-credentials: false @@ -41,7 +46,7 @@ jobs: - name: Generate GitHub App token id: generate_token - uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 with: app-id: 800408 # Yubico Docs owner: Yubico @@ -49,13 +54,13 @@ jobs: private-key: ${{ secrets.GH_APP_YUBICO_DOCS_PRIVATE_KEY }} - name: Check out docs-gitops repo (${{ inputs.gitops-branch }} branch) - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: Yubico/docs-gitops ref: ${{ inputs.gitops-branch }} token: ${{ steps.generate_token.outputs.token }} path: gitops - persist-credentials: false + persist-credentials: true - name: Update GitOps resources run: sed -i "s#/yesdk/yesdk-docserver:.*\$#/yesdk/yesdk-docserver:$IMAGE_TAG#" ./k8s/yesdk/kustomization.yaml @@ -82,8 +87,13 @@ jobs: needs: deploy steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + - name: Checkout - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -95,7 +105,7 @@ jobs: - name: Generate GitHub App token id: generate_token - uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 with: app-id: 260767 # Yubico Commit Status Reader owner: Yubico diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 0eb0df465..730c19d8f 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -34,21 +34,26 @@ jobs: # actions: read steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + - name: "Checkout code" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif - # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: - # - you want to enable the Branch-Protection check on a *public* repository, or - # - you are installing Scorecard on a *private* repository - # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. - # repo_token: ${{ secrets.SCORECARD_TOKEN }} + # Fine-grained PAT token required to enable Branch-Protection check. + # The token must have "Administration: Read-only" permission. + # To create the PAT, follow: https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional + # Add the token as a repository secret named SCORECARD_TOKEN. + repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers @@ -65,7 +70,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: SARIF file path: results.sarif @@ -74,6 +79,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 with: sarif_file: results.sarif diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml index f414e7b4e..6726c769c 100644 --- a/.github/workflows/test-macos.yml +++ b/.github/workflows/test-macos.yml @@ -18,6 +18,9 @@ on: workflow_dispatch: workflow_call: +permissions: + contents: read + jobs: test: name: MacOS @@ -27,10 +30,15 @@ jobs: packages: read steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: global-json-file: "./global.json" @@ -63,7 +71,7 @@ jobs: run: dotnet test Yubico.Core/tests/Yubico.Core.UnitTests.csproj --filter "FullyQualifiedName!~DisposalTests" --logger trx --settings coverlet.runsettings.xml --collect:"XPlat Code Coverage" - name: Upload Test Result Files - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: TestResults-macOS path: '**/TestResults/*' diff --git a/.github/workflows/test-ubuntu.yml b/.github/workflows/test-ubuntu.yml index 9a0bf445a..8e92dc127 100644 --- a/.github/workflows/test-ubuntu.yml +++ b/.github/workflows/test-ubuntu.yml @@ -18,6 +18,9 @@ on: workflow_dispatch: workflow_call: +permissions: + contents: read + jobs: test: name: Ubuntu @@ -27,10 +30,15 @@ jobs: packages: read steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: global-json-file: "./global.json" @@ -49,7 +57,7 @@ jobs: run: dotnet test Yubico.Core/tests/Yubico.Core.UnitTests.csproj --logger trx --settings coverlet.runsettings.xml --collect:"XPlat Code Coverage" - name: Upload Test Result Files - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: TestResults-Ubuntu path: '**/TestResults/*' diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 0c9d90be4..88f28a916 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -18,6 +18,9 @@ on: workflow_dispatch: workflow_call: +permissions: + contents: read + jobs: test: name: Windows @@ -27,10 +30,15 @@ jobs: contents: read steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: global-json-file: "./global.json" @@ -44,7 +52,7 @@ jobs: run: dotnet test Yubico.Core/tests/Yubico.Core.UnitTests.csproj --logger trx --settings coverlet.runsettings.xml --collect:"XPlat Code Coverage" - name: Upload Test Result Files - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: TestResults-Windows path: '**/TestResults/*' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7390c18c7..8c5d0096d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,11 +59,9 @@ on: # - '.github/workflows/test.yml' permissions: - pull-requests: write contents: read - checks: write - packages: read - + packages: read # Required by reusable test workflows to access GitHub Packages + jobs: test-windows: name: Tests @@ -82,9 +80,14 @@ jobs: if: inputs.build-coverage-report == true steps: - - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - name: Combine Coverage Reports # This is because one report is produced per project, and we want one result for all of them. - uses: danielpalme/ReportGenerator-GitHub-Action@dcdfb6e704e87df6b2ed0cf123a6c9f69e364869 # 5.5.0 + uses: danielpalme/ReportGenerator-GitHub-Action@ee0ae774f6d3afedcbd1683c1ab21b83670bdf8e # 5.5.1 with: reports: "**/*.cobertura.xml" # REQUIRED # The coverage reports that should be parsed (separated by semicolon). Globbing is supported. targetdir: "${{ github.workspace }}" # REQUIRED # The directory where the generated report should be saved. @@ -109,20 +112,29 @@ jobs: thresholds: "40 60" - name: Upload Code Coverage Report - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: CoverageResults path: code-coverage-results.md - pr-comment-coverage-results: + pr-comment-coverage-results: name: "Add PR Comment: Coverage Results" runs-on: ubuntu-latest - needs: build-coverage-report + needs: build-coverage-report + # Requires write permission to comment on PRs with coverage results + permissions: + pull-requests: write # Required to add/update PR comments + contents: read if: github.event_name == 'pull_request' steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + - name: Download coverage results - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: CoverageResults @@ -132,29 +144,39 @@ jobs: recreate: true path: code-coverage-results.md - pr-comment-test-results: + pr-comment-test-results: name: "Add PR Comment: Test Results" runs-on: ubuntu-latest - needs: [test-windows, test-ubuntu, test-macos] + needs: [test-windows, test-ubuntu, test-macos] + # Requires write permissions to publish test results to PR + permissions: + checks: write # Required to create check runs for test results + pull-requests: write # Required to add/update PR comments + contents: read if: github.event_name == 'pull_request' steps: - - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - name: "Add PR Comment: Test Results (Windows)" - uses: EnricoMi/publish-unit-test-result-action@34d7c956a59aed1bfebf31df77b8de55db9bbaaf # v2.21.0 + uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f # v2.22.0 with: trx_files: "${{ github.workspace }}/TestResults-Windows/**/*.trx" check_name: "Test Results: Windows" - name: "Add PR Comment: Test Results (Ubuntu)" - uses: EnricoMi/publish-unit-test-result-action@34d7c956a59aed1bfebf31df77b8de55db9bbaaf # v2.21.0 + uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f # v2.22.0 with: trx_files: "${{ github.workspace }}/TestResults-Ubuntu/**/*.trx" check_name: "Test Results: Ubuntu" - name: "Add PR Comment: Test Results (MacOS)" - uses: EnricoMi/publish-unit-test-result-action@34d7c956a59aed1bfebf31df77b8de55db9bbaaf # v2.21.0 + uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f # v2.22.0 with: trx_files: "${{ github.workspace }}/TestResults-macOS/**/*.trx" check_name: "Test Results: MacOS" \ No newline at end of file diff --git a/.github/workflows/upload-docs.yml b/.github/workflows/upload-docs.yml index fb3b0e5c1..4a67844e9 100644 --- a/.github/workflows/upload-docs.yml +++ b/.github/workflows/upload-docs.yml @@ -44,12 +44,17 @@ jobs: image-hash: ${{ steps.push_image.outputs.imagehash }} steps: # Checkout the local repository as we need the Dockerfile and other things even for this step. - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # Grab the just-built documentation artifact and inflate the archive at the expected location. - - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: Documentation path: docs/_site/ diff --git a/.github/workflows/verify-code-style.yml b/.github/workflows/verify-code-style.yml index b1fd15548..45f1f11f4 100644 --- a/.github/workflows/verify-code-style.yml +++ b/.github/workflows/verify-code-style.yml @@ -27,16 +27,24 @@ on: # - '**.csproj' # - '**.sln' # - '.github/workflows/check-code-formatting.yml' +permissions: + contents: read + jobs: verify-code-style: name: "Verify code style" runs-on: windows-latest steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: global-json-file: "./global.json" source-url: https://nuget.pkg.github.com/Yubico/index.json diff --git a/Dockerfile b/Dockerfile index bea9986d0..90396f3ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM nginx:alpine +FROM nginx:alpine@sha256:2622096d277282954bcf28459f8a3bc527c34ec8f4847fb46ec11f7ed49efd8b ARG UID=1000 ARG GID=1000 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..8ecfe583f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,28 @@ +# Security +Yubico is committed to maintaining the security and privacy of our products, services, and customers. +We value the contributions of the security research community and have implemented a Coordinated Vulnerability +Disclosure process to handle vulnerability disclosures in a manner that protects the community while encouraging +responsible reporting. + +# Reporting Security Issues + +If you believe you have found a security vulnerability in any Yubico-owned repository, **please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests**, instead report it to us as described below. + +* __Email Us__: Send details to **security@yubico.com** . + Include: + - A description of the issue and steps to reproduce it. + - Any supporting evidence (e.g., screenshots, proof of concept). + - Any potential impact and your recommendations for mitigating the risk. + - Your contact information (optional if you wish to remain anonymous). +* __Encrypt Sensitive Data__: + Use our PGP key at __[Yubico-Security-Team-Public-Key](https://www.yubico.com/.well-known/Yubico-Security-Team-Public.asc)__ if needed. +* __Act Responsibly__: We request that you not share information about a potential vulnerability with others until we have had a chance to triage and if needed, provide mitigation to impacted customers. + +# Our Process +* __Confirmation__: We will confirm receipt of the report within 3 business days. +* __Triage__: We will work with you to validate and resolve the issue. +* __Disclosure__: We will notify you when the fix is complete and may coordinate public disclosure timelines, if applicable. +* __Acknowledgement__: If desired, we will acknowledge your contribution as part of the disclosure process. + +# Safe Harbor +Yubico will not pursue or support any legal action related to the research and disclosure of vulnerability when those activities follow Yubico’s coordinated vulnerability disclosure policy and process. diff --git a/Yubico.Core/src/Yubico.Core.csproj b/Yubico.Core/src/Yubico.Core.csproj index 544e1515e..02744a08e 100644 --- a/Yubico.Core/src/Yubico.Core.csproj +++ b/Yubico.Core/src/Yubico.Core.csproj @@ -113,14 +113,14 @@ limitations under the License. --> - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + @@ -136,6 +136,7 @@ limitations under the License. --> + <_Parameter1>Yubico.Core.UnitTests,PublicKey=00240000048000001401000006020000002400005253413100080000010001003312c63e1417ad4652242148c599b55c50d3213c7610b4cc1f467b193bfb8d131de6686268a9db307fcef9efcd5e467483fe9015307e5d0cf9d2fd4df12f29a1c7a72e531d8811ca70f6c80c4aeb598c10bb7fc48742ab86aa7986b0ae9a2f4876c61e0b81eb38e5b549f1fc861c633206f5466bfde021cb08d094742922a8258b582c3bc029eab88c98d476dac6e6f60bc0016746293f5337c68b22e528931b6494acddf1c02b9ea3986754716a9f2a32c59ff3d97f1e35ee07ca2972b0269a4cde86f7b64f80e7c13152c0f84083b5cc4f06acc0efb4316ff3f08c79bc0170229007fb27c97fb494b22f9f7b07f45547e263a44d5a7fe7da6a945a5e47afc9 diff --git a/Yubico.Core/src/Yubico/Core/Devices/SmartCard/DesktopSmartCardConnection.cs b/Yubico.Core/src/Yubico/Core/Devices/SmartCard/DesktopSmartCardConnection.cs index fd989196c..f8a6f1935 100644 --- a/Yubico.Core/src/Yubico/Core/Devices/SmartCard/DesktopSmartCardConnection.cs +++ b/Yubico.Core/src/Yubico/Core/Devices/SmartCard/DesktopSmartCardConnection.cs @@ -146,11 +146,11 @@ public ResponseApdu Transmit(CommandApdu commandApdu) // The YubiKey likely will never return a buffer larger than 512 bytes without instead // using response chaining. byte[] outputBuffer = new byte[512]; - + byte[] apduBytes = commandApdu.AsByteArray(); uint result = SCardTransmit( _cardHandle, new SCARD_IO_REQUEST(_activeProtocol), - commandApdu.AsByteArray(), + apduBytes, IntPtr.Zero, outputBuffer, out int outputBufferSize diff --git a/Yubico.Core/tests/Yubico.Core.UnitTests.csproj b/Yubico.Core/tests/Yubico.Core.UnitTests.csproj index ffe5d4959..0a9ce4e91 100644 --- a/Yubico.Core/tests/Yubico.Core.UnitTests.csproj +++ b/Yubico.Core/tests/Yubico.Core.UnitTests.csproj @@ -46,7 +46,7 @@ limitations under the License. --> - + diff --git a/Yubico.YubiKey/examples/PivSampleCode/CertificateOperations/YubiKeySignatureGenerator.cs b/Yubico.YubiKey/examples/PivSampleCode/CertificateOperations/YubiKeySignatureGenerator.cs index 55165c0d1..732bb072d 100644 --- a/Yubico.YubiKey/examples/PivSampleCode/CertificateOperations/YubiKeySignatureGenerator.cs +++ b/Yubico.YubiKey/examples/PivSampleCode/CertificateOperations/YubiKeySignatureGenerator.cs @@ -127,30 +127,28 @@ public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm) } // Compute the message digest of the data using the given hashAlgorithm. - private byte[] DigestData(byte[] data, HashAlgorithmName hashAlgorithm) + // For RSA keys, returns the raw digest (PadRsa handles signature padding). + // For ECC keys, pads the digest to key size with leading zeros if needed. + public byte[] DigestData(byte[] data, HashAlgorithmName hashAlgorithm) { - using HashAlgorithm digester = hashAlgorithm.Name switch + byte[] digest = MessageDigestOperations.ComputeMessageDigest(data, hashAlgorithm); + + // For RSA, return the raw digest - PadRsa handles the signature padding + if (_algorithm.IsRSA()) { - "SHA1" => CryptographyProviders.Sha1Creator(), - "SHA256" => CryptographyProviders.Sha256Creator(), - "SHA384" => CryptographyProviders.Sha384Creator(), - "SHA512" => CryptographyProviders.Sha512Creator(), - _ => throw new ArgumentException( - string.Format( - CultureInfo.CurrentCulture, - InvalidAlgorithmMessage)), - }; + return digest; + } - // If the algorithm is P-256, then make sure the digest is exactly 32 - // bytes. If it's P-384, the digest must be exactly 48 bytes. - // We'll prepend 00 bytes if necessary. - int bufferSize = _algorithm.GetKeySizeBytes(); + // For ECC, the digest must match the key size (e.g., 32 bytes for P-256) + // Pad with leading zeros if necessary + int keySizeBytes = _algorithm.GetKeySizeBytes(); - byte[] digest = new byte[bufferSize]; - int offset = bufferSize - (digester.HashSize / 8); + if (digest.Length == keySizeBytes) + { + return digest; + } - // If offset < 0, that means the digest is too big. - if (offset < 0) + if (digest.Length > keySizeBytes) { throw new ArgumentException( string.Format( @@ -158,10 +156,12 @@ private byte[] DigestData(byte[] data, HashAlgorithmName hashAlgorithm) InvalidAlgorithmMessage)); } - _ = digester.TransformFinalBlock(data, 0, data.Length); - Array.Copy(digester.Hash, 0, digest, offset, digest.Length); + // Pad with leading zeros + byte[] paddedDigest = new byte[keySizeBytes]; + int offset = keySizeBytes - digest.Length; + Array.Copy(digest, 0, paddedDigest, offset, digest.Length); - return digest; + return paddedDigest; } // Create a block of data that is the data to sign padded following the diff --git a/Yubico.YubiKey/src/Yubico.YubiKey.csproj b/Yubico.YubiKey/src/Yubico.YubiKey.csproj index 6549865c3..dc3d44ef3 100644 --- a/Yubico.YubiKey/src/Yubico.YubiKey.csproj +++ b/Yubico.YubiKey/src/Yubico.YubiKey.csproj @@ -104,13 +104,13 @@ limitations under the License. --> - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -123,10 +123,10 @@ limitations under the License. --> all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionManager.cs b/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionManager.cs deleted file mode 100644 index f62977ca5..000000000 --- a/Yubico.YubiKey/src/Yubico/YubiKey/ConnectionManager.cs +++ /dev/null @@ -1,318 +0,0 @@ -// Copyright 2025 Yubico AB -// -// Licensed under the Apache License, Version 2.0 (the "License"). -// You may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using Yubico.Core.Devices; -using Yubico.Core.Devices.Hid; -using Yubico.Core.Devices.SmartCard; - -namespace Yubico.YubiKey -{ - /// - /// A service class that tracks open connections to the YubiKeys on the device. - /// - /// - /// - /// The YubiKey is fundamentally a single threaded device. Even if you are connecting through different USB interfaces, - /// such as Smart Card and HID Keyboard, the YubiKey can still only process a single request at a time. Device event - /// notifications now introduces implicit multi-threading into the SDK. We now need to be more aware of the active - /// connection state of any particular YubiKey. Even an enumeration can be disruptive, since we need to interrogate - /// the device in order to find its device properties. - /// - /// - /// This class is meant to be used as a service / singleton within the SDK. Whenever you need to create a connection - /// to a YubiKey, call the - /// method. Usage of this class should avoid the creation of more than one connection to a single physical YubiKey. - /// Connecting to different YubiKeys at once is fine, just not to a single key. - /// - /// - // JUSTIFICATION: This class is a singleton, which means its lifetime will span the process lifetime. It contains - // a lock which is disposable, so we must call its Dispose method at some point. The only reasonable time to do that - // is in this class's finalizer. This analyzer doesn't seem to see this and still warns. -#pragma warning disable CA1001 - internal class ConnectionManager -#pragma warning restore CA1001 - { - // Easy thread-safe singleton pattern using Lazy<> - private static readonly Lazy _instance = - new Lazy(() => new ConnectionManager()); - - /// - /// Gets the process-global singleton instance of the connection manager. - /// - public static ConnectionManager Instance => _instance.Value; - - /// - /// Determines if the given device object supports the requested YubiKey application. - /// - /// A concrete device object from Yubico.Core. - /// An application of the YubiKey. - /// `true` if the device object supports the application, `false` otherwise. - // This function uses C# 8.0 pattern matching to build a concise table. - public static bool DeviceSupportsApplication(IDevice device, YubiKeyApplication application) => - (device, application) switch - { - // FIDO interface - (IHidDevice { UsagePage: HidUsagePage.Fido }, YubiKeyApplication.FidoU2f) => true, - (IHidDevice { UsagePage: HidUsagePage.Fido }, YubiKeyApplication.Fido2) => true, - // Keyboard interface - (IHidDevice { UsagePage: HidUsagePage.Keyboard }, YubiKeyApplication.Otp) => true, - // All Smart Card based interfaces - (ISmartCardDevice _, YubiKeyApplication.Management) => true, - (ISmartCardDevice _, YubiKeyApplication.Oath) => true, - (ISmartCardDevice _, YubiKeyApplication.Piv) => true, - (ISmartCardDevice _, YubiKeyApplication.OpenPgp) => true, - (ISmartCardDevice _, YubiKeyApplication.InterIndustry) => true, - (ISmartCardDevice _, YubiKeyApplication.SecurityDomain) => true, - (ISmartCardDevice _, YubiKeyApplication.YubiHsmAuth) => true, - // NB: Certain past models of YK NEO and YK 4 supported these applications over CCID - (ISmartCardDevice _, YubiKeyApplication.FidoU2f) => true, - (ISmartCardDevice _, YubiKeyApplication.Fido2) => true, - (ISmartCardDevice _, YubiKeyApplication.Otp) => true, - // NFC interface - (ISmartCardDevice { Kind: SmartCardConnectionKind.Nfc }, YubiKeyApplication.OtpNdef) => true, - _ => false - }; - - private readonly HashSet _openConnections = new HashSet(); - private readonly ReaderWriterLockSlim _hashSetLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); - - /// - /// Finalizes the ConnectionManager singleton - /// - /// - /// As this class contains a disposable resource, we need some place to properly dispose of it. The finalizer - /// is the only real chance we get to invoke this. - /// - ~ConnectionManager() - { - _hashSetLock.Dispose(); - } - - /// - /// Tries to connect to a known application on the YubiKey, failing if a connection to that YubiKey (regardless - /// of the application) already exists. - /// - /// - /// The YubiKey that we are attempting to connect to. - /// - /// - /// The actual physical device exposed by the YubiKey that we're attempting to connect to. - /// - /// - /// The YubiKey application that we're attempting to connect to. - /// - /// - /// The connection, if established. `null` otherwise. - /// - /// - /// 'true' if the connection was established, 'false' if there is already an outstanding connection to this - /// device. Note that this method throws exceptions for all other failure modes. - /// - /// - /// The specified device does not support requested application. - /// - /// - /// The device type is not recognized. - /// - public bool TryCreateConnection( - IYubiKeyDevice yubiKeyDevice, - IDevice device, - YubiKeyApplication application, - [MaybeNullWhen(returnValue: false)] - out IYubiKeyConnection connection) - { - if (!DeviceSupportsApplication(device, application)) - { - throw new ArgumentException( - ExceptionMessages.DeviceDoesNotSupportApplication, - nameof(application)); - } - - // Since taking a write lock is potentially very expensive, let's try and make a best effort to see if the - // YubiKey is already present. This way we can fail fast. - _hashSetLock.EnterReadLock(); - - try - { - if (_openConnections.Contains(yubiKeyDevice)) - { - connection = null; - - return false; - } - } - finally - { - _hashSetLock.ExitReadLock(); - } - - // The YubiKey wasn't present just a few microseconds ago, so hopefully we will be able to succeed in - // establishing a connection to it. - _hashSetLock.EnterWriteLock(); - - try - { - // We still need to double check that the key wasn't added in the time between exiting the read lock - // and entering the write lock. - if (_openConnections.Contains(yubiKeyDevice)) - { - connection = null; - - return false; - } - - connection = device switch - { - IHidDevice { UsagePage: HidUsagePage.Fido } d => new FidoConnection(d), - IHidDevice { UsagePage: HidUsagePage.Keyboard } d => new KeyboardConnection(d), - ISmartCardDevice d => new SmartCardConnection(d, application), - _ => throw new NotSupportedException(ExceptionMessages.DeviceTypeNotRecognized) - }; - - _ = _openConnections.Add(yubiKeyDevice); - } - finally - { - _hashSetLock.ExitWriteLock(); - } - - return true; - } - - /// - /// Tries to connect to an arbitrary application on the YubiKey, failing if a connection to that YubiKey already - /// exists. - /// - /// - /// The YubiKey that we are attempting to connect to. - /// - /// - /// The actual physical device exposed by the YubiKey that we're attempting to connect to. - /// - /// - /// The smart card application ID (AID) to connect to. - /// - /// - /// The connection, if established. `null` otherwise. - /// - /// - /// 'true' if the connection was established, 'false' if there is already an outstanding connection to this - /// device. Note that this method throws exceptions for all other failure modes. - /// - /// - /// The platform device specified in is not a smart-card based, or is not a device - /// capable of selecting arbitrary applications. - /// - public bool TryCreateConnection( - IYubiKeyDevice yubiKeyDevice, - IDevice device, - byte[] applicationId, - [MaybeNullWhen(returnValue: false)] - out IYubiKeyConnection connection) - { - var smartCardDevice = device as ISmartCardDevice ?? throw new ArgumentException( - ExceptionMessages.DeviceDoesNotSupportApplication, - nameof(applicationId)); - - // Since taking a write lock is potentially very expensive, let's try and make a best effort to see if the - // YubiKey is already present. This way we can fail fast. - _hashSetLock.EnterReadLock(); - - try - { - if (_openConnections.Contains(yubiKeyDevice)) - { - connection = null; - - return false; - } - } - finally - { - _hashSetLock.ExitReadLock(); - } - - // The YubiKey wasn't present just a few microseconds ago, so hopefully we will be able to succeed in - // establishing a connection to it. - _hashSetLock.EnterWriteLock(); - - try - { - // We still need to double check that the key wasn't added in the time between exiting the read lock - // and entering the write lock. - if (_openConnections.Contains(yubiKeyDevice)) - { - connection = null; - - return false; - } - - connection = new SmartCardConnection(smartCardDevice, applicationId); - _ = _openConnections.Add(yubiKeyDevice); - } - finally - { - _hashSetLock.ExitWriteLock(); - } - - return true; - } - - /// - /// Ends the tracking of a YubiKey connection. - /// - /// - /// The YubiKey to disconnect from. - /// - /// - /// Thrown if the specified YubiKey does not have any active connections. - /// - public void EndConnection(IYubiKeyDevice yubiKeyDevice) - { - // Since taking a write lock is potentially very expensive, let's try and make a best effort to see if the - // YubiKey is still present. This way we can fail fast. - _hashSetLock.EnterReadLock(); - - try - { - if (!_openConnections.Contains(yubiKeyDevice)) - { - throw new KeyNotFoundException(ExceptionMessages.NoActiveConnections); - } - } - finally - { - _hashSetLock.ExitReadLock(); - } - - _hashSetLock.EnterWriteLock(); - - try - { - if (!_openConnections.Remove(yubiKeyDevice)) - { - throw new KeyNotFoundException(ExceptionMessages.NoActiveConnections); - } - } - finally - { - _hashSetLock.ExitWriteLock(); - } - } - } -} diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/AuthenticatorOperationParameters.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/AuthenticatorOperationParameters.cs index 599305369..856fafa98 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/AuthenticatorOperationParameters.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/AuthenticatorOperationParameters.cs @@ -80,8 +80,8 @@ public abstract class AuthenticatorOperationParameters : I /// public void AddExtension(string extensionKey, TValue value) { - Guard.IsNotNullOrWhiteSpace(extensionKey, nameof(extensionKey)); - Guard.IsNotNull(value, nameof(value)); + Guard.IsNotNullOrWhiteSpace(extensionKey); + Guard.IsNotNull(value); _extensions[extensionKey] = value switch { @@ -95,7 +95,9 @@ public void AddExtension(string extensionKey, TValue value) ICborEncode cborEncode => cborEncode.CborEncode(), _ => throw new ArgumentException(ExceptionMessages.Ctap2CborUnexpectedValue, nameof(value)) }; - + + return; + static byte[] EncodeValue(Action writeAction) { var cbor = new CborWriter(CborConformanceMode.Ctap2Canonical, convertIndefiniteLengthEncodings: true); @@ -103,34 +105,7 @@ static byte[] EncodeValue(Action writeAction) return cbor.Encode(); } } - - /// - /// Add an entry to the extensions list. - /// - /// - /// - /// Each extension is a key/value pair. For each extension the key is a - /// string (such as "credProtect" or "hmac-secret"). However, each value - /// is different. There will be a definition of the value that - /// goes with each key. It will be possible to encode that definition - /// using the rules of CBOR. The caller supplies the key and the encoded value. - /// - /// - /// - /// The key of key/value to add. - /// - /// - /// The CBOR-encoded value of key/value to add. - /// - /// - /// The extensionKey or encodedValue arg is null. - /// - public void AddExtension(string extensionKey, byte[] encodedBytes) - { - Guard.IsNotNull(encodedBytes, nameof(encodedBytes)); - _extensions[extensionKey] = encodedBytes; - } - + /// /// Add an entry to the list of options. /// diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.LargeBlobs.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.LargeBlobs.cs index b53873494..671564a95 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.LargeBlobs.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.LargeBlobs.cs @@ -236,7 +236,6 @@ public void SetSerializedLargeBlobArray(SerializedLargeBlobArray serializedLarge int remaining = encodedArray.Length; int maxFragmentLength = AuthenticatorInfo.MaximumMessageSize ?? AuthenticatorInfo.DefaultMaximumMessageSize; maxFragmentLength -= MessageOverhead; - int currentLength; bool forceToken = false; using HashAlgorithm digester = CryptographyProviders.Sha256Creator(); @@ -247,7 +246,7 @@ public void SetSerializedLargeBlobArray(SerializedLargeBlobArray serializedLarge forceToken, PinUvAuthTokenPermissions.LargeBlobWrite, null); currentToken.CopyTo(token.AsMemory()); - currentLength = remaining >= maxFragmentLength ? maxFragmentLength : remaining; + int currentLength = remaining >= maxFragmentLength ? maxFragmentLength : remaining; byte[] dataToAuth = BuildDataToAuth(encodedArray, offset, currentLength, digester); byte[] pinUvAuthParam = AuthProtocol.AuthenticateUsingPinToken(token, 0, currentToken.Length, dataToAuth); diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs index 17757e259..8ea5aa870 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/SmartCardConnection.cs @@ -123,9 +123,11 @@ public virtual TResponse SendCommand(IYubiKeyCommand yubiK SelectApplication(); } + var commandApdu = yubiKeyCommand.CreateCommandApdu(); + var commandType = yubiKeyCommand.GetType(); var responseApdu = _apduPipeline.Invoke( - yubiKeyCommand.CreateCommandApdu(), - yubiKeyCommand.GetType(), + commandApdu, + commandType, typeof(TResponse)); return yubiKeyCommand.CreateResponseForApdu(responseApdu); diff --git a/Yubico.YubiKey/tests/integration/Yubico.YubiKey.IntegrationTests.csproj b/Yubico.YubiKey/tests/integration/Yubico.YubiKey.IntegrationTests.csproj index 991101245..1f710b871 100644 --- a/Yubico.YubiKey/tests/integration/Yubico.YubiKey.IntegrationTests.csproj +++ b/Yubico.YubiKey/tests/integration/Yubico.YubiKey.IntegrationTests.csproj @@ -32,21 +32,21 @@ limitations under the License. --> - + - - + + - + - + diff --git a/Yubico.YubiKey/tests/sandbox/Yubico.YubiKey.TestApp.csproj b/Yubico.YubiKey/tests/sandbox/Yubico.YubiKey.TestApp.csproj index 2800bcbf9..0255a7c8a 100644 --- a/Yubico.YubiKey/tests/sandbox/Yubico.YubiKey.TestApp.csproj +++ b/Yubico.YubiKey/tests/sandbox/Yubico.YubiKey.TestApp.csproj @@ -32,11 +32,11 @@ limitations under the License. --> - + - - + + diff --git a/Yubico.YubiKey/tests/unit/Yubico.YubiKey.UnitTests.csproj b/Yubico.YubiKey/tests/unit/Yubico.YubiKey.UnitTests.csproj index 9d8971aec..894414423 100644 --- a/Yubico.YubiKey/tests/unit/Yubico.YubiKey.UnitTests.csproj +++ b/Yubico.YubiKey/tests/unit/Yubico.YubiKey.UnitTests.csproj @@ -37,12 +37,12 @@ limitations under the License. --> - + - + PreserveNewest diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/ConnectionManagerTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/ConnectionManagerTests.cs deleted file mode 100644 index 3e8a73eb6..000000000 --- a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/ConnectionManagerTests.cs +++ /dev/null @@ -1,324 +0,0 @@ -// Copyright 2025 Yubico AB -// -// Licensed under the Apache License, Version 2.0 (the "License"). -// You may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using System.Collections.Generic; -using NSubstitute; -using Xunit; -using Yubico.Core.Devices; -using Yubico.Core.Devices.Hid; -using Yubico.Core.Devices.SmartCard; -using Yubico.Core.Iso7816; - -namespace Yubico.YubiKey -{ - internal class TestSmartCardDevice : ISmartCardDevice - { - public readonly static ISmartCardDevice AnyInstance = new TestSmartCardDevice() - { Kind = SmartCardConnectionKind.Any }; - - public readonly static ISmartCardDevice NfcInstance = new TestSmartCardDevice() - { Kind = SmartCardConnectionKind.Nfc }; - - public DateTime LastAccessed { get; } = DateTime.Now; - public string Path { get; } = string.Empty; - public string? ParentDeviceId { get; } = null; - public AnswerToReset? Atr { get; } - public SmartCardConnectionKind Kind { get; private set; } - public ISmartCardConnection Connect() - { - throw new System.NotImplementedException(); - } - } - - internal class TestHidDevice : IHidDevice - { - public readonly static IHidDevice FidoInstance = new TestHidDevice() { UsagePage = HidUsagePage.Fido }; - public readonly static IHidDevice KeyboardInstance = new TestHidDevice() { UsagePage = HidUsagePage.Keyboard }; - - public DateTime LastAccessed { get; } = DateTime.Now; - public string Path { get; } = string.Empty; - public string? ParentDeviceId { get; } = null; - public short VendorId { get; } - public short ProductId { get; } - public short Usage { get; } - public HidUsagePage UsagePage { get; private set; } - public IHidConnection ConnectToFeatureReports() - { - throw new System.NotImplementedException(); - } - - public IHidConnection ConnectToIOReports() - { - throw new System.NotImplementedException(); - } - } - - public class ConnectionManagerTests - { - private readonly IYubiKeyDevice _yubiKeyDeviceMock = Substitute.For(); - private readonly ISmartCardDevice _smartCardDeviceMock = Substitute.For(); - private readonly ISmartCardConnection _smartCardConnectionMock = Substitute.For(); - - public static IEnumerable SupportedApplicationTuples => - new List() - { - new object[] { TestHidDevice.FidoInstance, YubiKeyApplication.FidoU2f }, - new object[] { TestHidDevice.FidoInstance, YubiKeyApplication.Fido2 }, - new object[] { TestHidDevice.KeyboardInstance, YubiKeyApplication.Otp }, - new object[] { TestSmartCardDevice.AnyInstance, YubiKeyApplication.Otp }, - new object[] { TestSmartCardDevice.AnyInstance, YubiKeyApplication.Oath }, - new object[] { TestSmartCardDevice.AnyInstance, YubiKeyApplication.Piv }, - new object[] { TestSmartCardDevice.AnyInstance, YubiKeyApplication.OpenPgp }, - new object[] { TestSmartCardDevice.AnyInstance, YubiKeyApplication.FidoU2f }, - new object[] { TestSmartCardDevice.AnyInstance, YubiKeyApplication.Fido2 }, - new object[] { TestSmartCardDevice.AnyInstance, YubiKeyApplication.Management }, - new object[] { TestSmartCardDevice.AnyInstance, YubiKeyApplication.YubiHsmAuth }, - new object[] { TestSmartCardDevice.NfcInstance, YubiKeyApplication.OtpNdef } - }; - - [Theory] - [MemberData(nameof(SupportedApplicationTuples))] - public void DeviceSupportsApplication_GivenSupportedTuple_ReturnsTrue(IDevice device, YubiKeyApplication application) - { - Assert.True(ConnectionManager.DeviceSupportsApplication(device, application)); - } - - public static IEnumerable UnsupportedApplicationTuples => - new List() - { - new object[] { TestHidDevice.FidoInstance, YubiKeyApplication.Otp }, - new object[] { TestHidDevice.FidoInstance, YubiKeyApplication.OtpNdef }, - new object[] { TestHidDevice.FidoInstance, YubiKeyApplication.Oath }, - new object[] { TestHidDevice.FidoInstance, YubiKeyApplication.Piv }, - new object[] { TestHidDevice.FidoInstance, YubiKeyApplication.OpenPgp }, - new object[] { TestHidDevice.FidoInstance, YubiKeyApplication.Management }, - new object[] { TestHidDevice.FidoInstance, YubiKeyApplication.YubiHsmAuth }, - new object[] { TestHidDevice.KeyboardInstance, YubiKeyApplication.OtpNdef }, - new object[] { TestHidDevice.KeyboardInstance, YubiKeyApplication.Oath }, - new object[] { TestHidDevice.KeyboardInstance, YubiKeyApplication.Piv }, - new object[] { TestHidDevice.KeyboardInstance, YubiKeyApplication.OpenPgp }, - new object[] { TestHidDevice.KeyboardInstance, YubiKeyApplication.Management }, - new object[] { TestHidDevice.KeyboardInstance, YubiKeyApplication.YubiHsmAuth }, - }; - - [Theory] - [MemberData(nameof(UnsupportedApplicationTuples))] - public void DeviceSupportsApplication_GivenUnsupportedTuple_ReturnsFalse(IDevice device, YubiKeyApplication application) - { - Assert.False(ConnectionManager.DeviceSupportsApplication(device, application)); - } - - [Fact] - public void Instance_ReturnsSameInstanceOfConnectionManager() - { - ConnectionManager? connectionManager1 = ConnectionManager.Instance; - Assert.NotNull(connectionManager1); - - ConnectionManager? connectionManager2 = ConnectionManager.Instance; - Assert.Same(connectionManager1, connectionManager2); - } - - [Fact] - public void TryCreateConnection_NoOpenConnections_ReturnsTrueAndConnection() - { - var cm = new ConnectionManager(); - - _ = _smartCardDeviceMock.Connect().Returns(_smartCardConnectionMock); - _ = _smartCardConnectionMock.Transmit(Arg.Any()) - .Returns(new ResponseApdu(Array.Empty(), SWConstants.Success)); - - bool result = cm.TryCreateConnection( - _yubiKeyDeviceMock, - _smartCardDeviceMock, - YubiKeyApplication.Piv, - out IYubiKeyConnection? connection); - - Assert.True(result); - Assert.NotNull(connection); - } - - [Fact] - public void TryCreateConnection_OpenConnectionToSameYubiKey_ReturnsFalseAndNull() - { - var cm = new ConnectionManager(); - - _ = _yubiKeyDeviceMock.Equals(Arg.Any()) - .Returns(true); - _ = _smartCardDeviceMock.Connect().Returns(_smartCardConnectionMock); - _ = _smartCardConnectionMock.Transmit(Arg.Any()) - .Returns(new ResponseApdu(Array.Empty(), SWConstants.Success)); - - _ = cm.TryCreateConnection( - _yubiKeyDeviceMock, - _smartCardDeviceMock, - YubiKeyApplication.Piv, - out _); - - bool result = cm.TryCreateConnection( - _yubiKeyDeviceMock, - _smartCardDeviceMock, - YubiKeyApplication.Piv, - out IYubiKeyConnection? connection); - - Assert.False(result); - Assert.Null(connection); - } - - [Fact] - public void TryCreateConnection_OpenConnectionToDifferentYubiKey_ReturnsTrueAndConnection() - { - var cm = new ConnectionManager(); - - _ = _yubiKeyDeviceMock.Equals(Arg.Any()) - .Returns(false); - _ = _smartCardDeviceMock.Connect().Returns(_smartCardConnectionMock); - _ = _smartCardConnectionMock.Transmit(Arg.Any()) - .Returns(new ResponseApdu(Array.Empty(), SWConstants.Success)); - - bool result = cm.TryCreateConnection( - _yubiKeyDeviceMock, - _smartCardDeviceMock, - YubiKeyApplication.Piv, - out IYubiKeyConnection? connection1); - - Assert.True(result); - Assert.NotNull(connection1); - - result = cm.TryCreateConnection( - _yubiKeyDeviceMock, - _smartCardDeviceMock, - YubiKeyApplication.Piv, - out IYubiKeyConnection? connection2); - - Assert.True(result); - Assert.NotNull(connection2); - } - - [Fact] - public void TryCreateConnectionOverload_NoOpenConnections_ReturnsTrueAndConnection() - { - var cm = new ConnectionManager(); - - _ = _smartCardDeviceMock.Connect().Returns(_smartCardConnectionMock); - _ = _smartCardConnectionMock.Transmit(Arg.Any()) - .Returns(new ResponseApdu(Array.Empty(), SWConstants.Success)); - - bool result = cm.TryCreateConnection( - _yubiKeyDeviceMock, - _smartCardDeviceMock, - new byte[] { 1, 2, 3, 4 }, - out IYubiKeyConnection? connection); - - Assert.True(result); - Assert.NotNull(connection); - } - - [Fact] - public void TryCreateConnectionOverload_OpenConnectionToSameYubiKey_ReturnsFalseAndNull() - { - var cm = new ConnectionManager(); - - _ = _yubiKeyDeviceMock.Equals(Arg.Any()) - .Returns(true); - _ = _smartCardDeviceMock.Connect().Returns(_smartCardConnectionMock); - _ = _smartCardConnectionMock.Transmit(Arg.Any()) - .Returns(new ResponseApdu(Array.Empty(), SWConstants.Success)); - - _ = cm.TryCreateConnection( - _yubiKeyDeviceMock, - _smartCardDeviceMock, - new byte[] { 1, 2, 3, 4 }, - out _); - - bool result = cm.TryCreateConnection( - _yubiKeyDeviceMock, - _smartCardDeviceMock, - new byte[] { 1, 2, 3, 4 }, - out IYubiKeyConnection? connection); - - Assert.False(result); - Assert.Null(connection); - } - - [Fact] - public void TryCreateConnectionOverload_OpenConnectionToDifferentYubiKey_ReturnsTrueAndConnection() - { - var cm = new ConnectionManager(); - - _ = _yubiKeyDeviceMock.Equals(Arg.Any()) - .Returns(false); - _ = _smartCardDeviceMock.Connect().Returns(_smartCardConnectionMock); - _ = _smartCardConnectionMock.Transmit(Arg.Any()) - .Returns(new ResponseApdu(Array.Empty(), SWConstants.Success)); - - bool result = cm.TryCreateConnection( - _yubiKeyDeviceMock, - _smartCardDeviceMock, - new byte[] { 1, 2, 3, 4 }, - out IYubiKeyConnection? connection1); - - Assert.True(result); - Assert.NotNull(connection1); - - result = cm.TryCreateConnection( - _yubiKeyDeviceMock, - _smartCardDeviceMock, - new byte[] { 1, 2, 3, 4 }, - out IYubiKeyConnection? connection2); - - Assert.True(result); - Assert.NotNull(connection2); - } - - [Fact] - public void EndConnection_NoOpenConnections_ThrowsNotFoundException() - { - var cm = new ConnectionManager(); - - void Action() => cm.EndConnection(_yubiKeyDeviceMock); - - _ = Assert.Throws(Action); - } - - [Fact] - public void EndConnection_OpenConnectionToSameYubiKey_AllowsNewConnection() - { - var cm = new ConnectionManager(); - - _ = _yubiKeyDeviceMock.Equals(Arg.Any()) - .Returns(true); - _ = _smartCardDeviceMock.Connect().Returns(_smartCardConnectionMock); - _ = _smartCardConnectionMock.Transmit(Arg.Any()) - .Returns(new ResponseApdu(Array.Empty(), SWConstants.Success)); - - _ = cm.TryCreateConnection( - _yubiKeyDeviceMock, - _smartCardDeviceMock, - YubiKeyApplication.Piv, - out _); - - cm.EndConnection(_yubiKeyDeviceMock); - - bool result = cm.TryCreateConnection( - _yubiKeyDeviceMock, - _smartCardDeviceMock, - YubiKeyApplication.Piv, - out IYubiKeyConnection? connection); - - Assert.True(result); - Assert.NotNull(connection); - } - } -} diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Sample/YubiKeySignatureGeneratorTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Sample/YubiKeySignatureGeneratorTests.cs new file mode 100644 index 000000000..235ad31cb --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Sample/YubiKeySignatureGeneratorTests.cs @@ -0,0 +1,228 @@ +// Copyright 2025 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Security.Cryptography; +using Xunit; +using Yubico.YubiKey.Cryptography; + +namespace Yubico.YubiKey.Sample +{ + /// + /// Unit tests for YubiKeySignatureGenerator.DigestData logic. + /// These tests verify the fix for the regression introduced in commit 01d2a667 + /// where _algorithm.GetKeySizeBytes() was incorrectly used instead of the hash digest size. + /// + /// Since YubiKeySignatureGenerator is in an example project that cannot be referenced + /// from unit tests (strong naming issues), these tests verify the digest computation + /// logic directly using the same approach the fixed code uses. + /// + public class YubiKeySignatureGeneratorDigestDataTests + { + private static readonly byte[] TestData = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; + + /// + /// Computes a digest using the same logic as the fixed YubiKeySignatureGenerator.DigestData method. + /// For RSA: returns the raw digest. + /// For ECC: pads the digest to key size with leading zeros if needed. + /// + private static byte[] ComputeDigestData(byte[] data, HashAlgorithmName hashAlgorithm, KeyType keyType) + { + byte[] digest = ComputeMessageDigest(data, hashAlgorithm); + + // For RSA, return the raw digest - PadRsa handles the signature padding + if (keyType.IsRSA()) + { + return digest; + } + + // For ECC, the digest must match the key size (e.g., 32 bytes for P-256) + // Pad with leading zeros if necessary + int keySizeBytes = keyType.GetKeySizeBytes(); + + if (digest.Length == keySizeBytes) + { + return digest; + } + + if (digest.Length > keySizeBytes) + { + throw new ArgumentException("Digest is larger than key size"); + } + + // Pad with leading zeros + byte[] paddedDigest = new byte[keySizeBytes]; + int offset = keySizeBytes - digest.Length; + Array.Copy(digest, 0, paddedDigest, offset, digest.Length); + + return paddedDigest; + } + + /// + /// Computes a message digest using the same logic as MessageDigestOperations.ComputeMessageDigest. + /// + private static byte[] ComputeMessageDigest(byte[] dataToDigest, HashAlgorithmName hashAlgorithm) + { + using HashAlgorithm digester = hashAlgorithm.Name switch + { + "SHA1" => CryptographyProviders.Sha1Creator(), + "SHA256" => CryptographyProviders.Sha256Creator(), + "SHA384" => CryptographyProviders.Sha384Creator(), + "SHA512" => CryptographyProviders.Sha512Creator(), + _ => throw new ArgumentException("Unsupported algorithm"), + }; + + byte[] digest = new byte[digester.HashSize / 8]; + + _ = digester.TransformFinalBlock(dataToDigest, 0, dataToDigest.Length); + Array.Copy(digester.Hash!, 0, digest, 0, digest.Length); + + return digest; + } + + /// + /// Computes a digest using the OLD BUGGY logic that was in YubiKeySignatureGenerator.DigestData. + /// This is used to verify that the bug would cause failures. + /// + private static byte[] ComputeDigestDataBuggy(byte[] data, HashAlgorithmName hashAlgorithm, KeyType keyType) + { + using HashAlgorithm digester = hashAlgorithm.Name switch + { + "SHA1" => CryptographyProviders.Sha1Creator(), + "SHA256" => CryptographyProviders.Sha256Creator(), + "SHA384" => CryptographyProviders.Sha384Creator(), + "SHA512" => CryptographyProviders.Sha512Creator(), + _ => throw new ArgumentException("Unsupported algorithm"), + }; + + // BUG: This uses key size (256 bytes for RSA2048) instead of digest size (32 bytes for SHA256) + int bufferSize = keyType.GetKeySizeBytes(); + + byte[] digest = new byte[bufferSize]; + int offset = bufferSize - (digester.HashSize / 8); + + if (offset < 0) + { + throw new ArgumentException("Digest too big"); + } + + _ = digester.TransformFinalBlock(data, 0, data.Length); + // BUG: This tries to copy digest.Length (256) bytes from a 32-byte Hash array + Array.Copy(digester.Hash!, 0, digest, offset, digest.Length); + + return digest; + } + + [Theory] + [InlineData(KeyType.RSA2048, "SHA256", 32)] + [InlineData(KeyType.RSA2048, "SHA384", 48)] + [InlineData(KeyType.RSA2048, "SHA512", 64)] + [InlineData(KeyType.RSA1024, "SHA256", 32)] + [InlineData(KeyType.RSA3072, "SHA256", 32)] + [InlineData(KeyType.RSA4096, "SHA256", 32)] + public void DigestData_RSA_ReturnsCorrectDigestSize(KeyType keyType, string hashName, int expectedSize) + { + // Arrange + var hashAlgorithm = new HashAlgorithmName(hashName); + + // Act + byte[] digest = ComputeDigestData(TestData, hashAlgorithm, keyType); + + // Assert + Assert.Equal(expectedSize, digest.Length); + } + + [Fact] + public void DigestData_RSA2048_SHA256_FixedVersion_DoesNotThrow() + { + // This is the specific scenario from the bug report: + // RSA2048 with SHA256 - the fixed version should not throw + var exception = Record.Exception(() => + ComputeDigestData(TestData, HashAlgorithmName.SHA256, KeyType.RSA2048)); + + Assert.Null(exception); + } + + [Fact] + public void DigestData_RSA2048_SHA256_BuggyVersion_Throws() + { + // This demonstrates the bug: RSA2048 with SHA256 was throwing because + // it tried to copy 256 bytes (key size) from a 32-byte array (hash size) + Assert.Throws(() => + ComputeDigestDataBuggy(TestData, HashAlgorithmName.SHA256, KeyType.RSA2048)); + } + + [Theory] + [InlineData(KeyType.ECP256, "SHA256", 32)] // Digest matches key size + [InlineData(KeyType.ECP384, "SHA256", 48)] // Digest (32) padded to key size (48) + [InlineData(KeyType.ECP384, "SHA384", 48)] // Digest matches key size + [InlineData(KeyType.ECP521, "SHA256", 66)] // Digest (32) padded to key size (66) + [InlineData(KeyType.ECP521, "SHA384", 66)] // Digest (48) padded to key size (66) + [InlineData(KeyType.ECP521, "SHA512", 66)] // Digest (64) padded to key size (66) + public void DigestData_ECC_ReturnsCorrectDigestSize(KeyType keyType, string hashName, int expectedSize) + { + // Arrange + var hashAlgorithm = new HashAlgorithmName(hashName); + + // Act + byte[] digest = ComputeDigestData(TestData, hashAlgorithm, keyType); + + // Assert + Assert.Equal(expectedSize, digest.Length); + } + + [Theory] + [InlineData(KeyType.ECP256, "SHA384")] // SHA384 (48 bytes) > P-256 key size (32 bytes) + [InlineData(KeyType.ECP256, "SHA512")] // SHA512 (64 bytes) > P-256 key size (32 bytes) + public void DigestData_ECC_ThrowsWhenDigestLargerThanKeySize(KeyType keyType, string hashName) + { + // Arrange + var hashAlgorithm = new HashAlgorithmName(hashName); + + // Act & Assert + Assert.Throws(() => ComputeDigestData(TestData, hashAlgorithm, keyType)); + } + + [Theory] + [InlineData(KeyType.ECP384, "SHA256", 16)] // P-384 (48) - SHA256 (32) = 16 bytes padding + [InlineData(KeyType.ECP521, "SHA256", 34)] // P-521 (66) - SHA256 (32) = 34 bytes padding + [InlineData(KeyType.ECP521, "SHA384", 18)] // P-521 (66) - SHA384 (48) = 18 bytes padding + public void DigestData_ECC_PadsWithLeadingZeros(KeyType keyType, string hashName, int expectedPadding) + { + // Arrange + var hashAlgorithm = new HashAlgorithmName(hashName); + + // Act + byte[] digest = ComputeDigestData(TestData, hashAlgorithm, keyType); + + // Assert - first bytes should be zeros (padding) + for (int i = 0; i < expectedPadding; i++) + { + Assert.Equal(0, digest[i]); + } + + // The non-zero hash data should start after the padding + bool hasNonZeroData = false; + for (int i = expectedPadding; i < digest.Length; i++) + { + if (digest[i] != 0) + { + hasNonZeroData = true; + break; + } + } + Assert.True(hasNonZeroData, "Expected non-zero hash data after padding"); + } + } +} diff --git a/Yubico.YubiKey/tests/utilities/Yubico.YubiKey.TestUtilities.csproj b/Yubico.YubiKey/tests/utilities/Yubico.YubiKey.TestUtilities.csproj index 92975a931..fe3fd1d5b 100644 --- a/Yubico.YubiKey/tests/utilities/Yubico.YubiKey.TestUtilities.csproj +++ b/Yubico.YubiKey/tests/utilities/Yubico.YubiKey.TestUtilities.csproj @@ -31,9 +31,9 @@ limitations under the License. --> - + - + diff --git a/build/Versions.props b/build/Versions.props index ea0289488..00eccace8 100644 --- a/build/Versions.props +++ b/build/Versions.props @@ -33,7 +33,7 @@ for external milestones. - 1.15.0 + 1.15.1 + +# Third-party payment extension (thirdPartyPayment) + +CTAP 2.2 and YubiKeys with firmware 5.8+ support the [thirdPartyPayment extension](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#sctn-thirdPartyPayment-extension), which allows credentials to be used for payment authentication scenarios where the transaction initiator is not the relying party. + +For example, suppose a user creates a thirdPartyPayment-enabled credential with their bank. The user then purchases an item from an online merchant and pays using their bank account, and the transaction is validated with their bank credential on their YubiKey. + +According to the CTAP 2.2 standard, implementation of the payment authentication flow is up to the platform. See the W3C's [Secure Payment Confirmation (SPC)](https://www.w3.org/TR/secure-payment-confirmation/) for an example of a possible implementation. + +## Creating a thirdPartyPayment-enabled credential + +YubiKey credentials (discoverable and non-discoverable) can only be used for a third-party payment transaction if the credential itself is thirdPartyPayment-enabled, and enablement must occur when the credential is first created. Enablement simply means that the credential's thirdPartyPayment bit flag has been set to ``true``. + +Only YubiKeys with firmware version 5.8 and later support the thirdPartyPayment extension. To verify whether a particular YubiKey supports the feature, check the key's ``AuthenticatorInfo``: + +```C# +using (Fido2Session fido2Session = new Fido2Session(yubiKey)) +{ + if (fido2Session.AuthenticatorInfo.IsExtensionSupported(Extensions.ThirdPartyPayment)) + { + ... + } +} +``` + +### MakeCredential example with thirdPartyPayment + +To create a thirdPartyPayment-enabled credential, we must add the thirdPartyPayment extension to the parameters for ``MakeCredential()``: + +```C# +using (Fido2Session fido2Session = new Fido2Session(yubiKey)) +{ + // Your app's key collector, which will be used to check user presence and perform PIN/UV + // verification during MakeCredential(). + fido2Session.KeyCollector = SomeKeyCollectorDelegate; + + // Create the parameters for MakeCredential (relyingParty, userEntity, and clientDataHashValue + // set elsewhere). + var makeCredentialParameters = new MakeCredentialParameters(relyingParty, userEntity) + { + ClientDataHash = clientDataHashValue + + }; + + // Add the thirdPartyPayment extension plus the "rk" option (to make the credential discoverable). + makeCredentialParameters.AddThirdPartyPaymentExtension(); + makeCredentialParameters.AddOption(AuthenticatorOptions.rk, true); + + // Create the third-party payment enabled credential using the parameters set above. + MakeCredentialData credentialData = fido2Session.MakeCredential(makeCredentialParameters); +} +``` + +After calling ``MakeCredential()``, we can check the ``AuthenticatorData`` to verify extension enablement: + +```C# +using (Fido2Session fido2Session = new Fido2Session(yubiKey)) +{ + ... + // Returns true if the extension was enabled. + bool thirdPartyPaymentStatus = credentialData.AuthenticatorData.GetThirdPartyPaymentExtension(); +} +``` + +## Authenticating a third-party payment transaction + +To successfully authenticate a third-party payment transaction using a YubiKey, the following must occur: + +- The YubiKey credential used for ``GetAssertion`` must be [third-party payment enabled](#creating-a-thirdpartypayment-enabled-credential). +- The thirdPartyPayment extension must be added to the parameters for ``GetAssertion``. + +During ``GetAssertion``, the YubiKey will return a boolean value for the thirdPartyPayment extension. If both requirements have been met, the value returned will be ``true``. + +### GetAssertion example with thirdPartyPayment + +To get an assertion with thirdPartyPayment, do the following: + +```C# +using (Fido2Session fido2Session = new Fido2Session(yubiKey)) +{ + // Your app's key collector, which will be used to check user presence and perform PIN/UV + // verification during GetAssertion(). + fido2Session.KeyCollector = SomeKeyCollectorDelegate; + + // Create the parameters for GetAssertion (relyingParty and clientDataHashValue set elsewhere), + // and add the request for the thirdPartyPayment return value to the parameters. + var getAssertionParameters = new GetAssertionParameters(relyingParty, clientDataHashValue); + getAssertionParameters.RequestThirdPartyPayment(); + + // Get the assertion using the parameters set above. + IReadOnlyList assertionDataList = fido2Session.GetAssertions(getAssertionParameters); +} +``` + +After calling ``GetAssertions()``, we can check the state of the ThirdPartyPayment return value via the ``AuthenticatorData``: + +```C# +using (Fido2Session fido2Session = new Fido2Session(yubiKey)) +{ + ... + // If the YubiKey contains multiple credentials for the relyingParty, this returns the value + // from the first credential's data. True = third-party payment enabled. + bool thirdPartyPaymentValue = assertionDataList[0].AuthenticatorData.GetThirdPartyPaymentExtension(); +} +``` \ No newline at end of file diff --git a/docs/users-manual/application-piv/cert-size.md b/docs/users-manual/application-piv/cert-size.md index a8800a6c4..c7d2c4482 100644 --- a/docs/users-manual/application-piv/cert-size.md +++ b/docs/users-manual/application-piv/cert-size.md @@ -18,55 +18,40 @@ limitations under the License. --> # Maximum certificate sizes -It is possible to store up to 24 private key/certificate pairs in the PIV slots. However, -there are space limitations. +It is possible to store up to 24 private key/certificate pairs in the PIV slots for YubiKeys with firmware version 4.x and higher. However, there are limits to the size of each certificate and the total space available for all certificates. Once the total certificate storage space has been filled, you cannot load additional certificates onto the YubiKey. -In the real world, certificates are generally less than 1,000 bytes. Some large certs are -over 1,000 bytes, but rarely over 2,000. It is unlikely that you will run into limitations -on the YubiKey. +Keys, however, are stored in a separate, fixed memory layout. It is always possible to store 24 *keys* in a YubiKey's PIV application, as they are not subject to the certificate storage limits. -Nonetheless, these are the space limitations for certs in the PIV application on the -YubiKey. +> [!NOTE] +> In practice, the size of a key/certificate pair is determined by the choice of algorithm and key length (e.g. RSA 1024 vs RSA 4096), certificate complexity (e.g. use of OIDs, size attributes), the presence of PIV attestation objects, etc. ## Maximum size for a single certificate -| YubiKey Version | Maximum Size in Bytes | -|:---------------------:|:---------------------:| -| before 4.0 (e.g. NEO) | 2025 | -| 4.x | 3052 | -| 4.x FIPS | 3052 | -| 5.x | 3052 | -| 5.x FIPS | 3052 | +If you attempt to load a certificate that is larger than the YubiKey's maximum allowable certificate size (as indicated in the table below), the YubiKey will reject it, and the SDK will throw an exception. -## Total space available for certificates - -Although a YubiKey 5.x will allow a 3052-byte cert in one of the slots, it will not be -able to store 24 certs that big. +| YubiKey Model (and Firmware Version)| Maximum Size in Bytes | +|:-----------------------------------:|:---------------------:| +| YubiKey NEO (prior to 4.x) | 2025 | +| YubiKey 4 Series (4.x) | 3052 | +| YubiKey 4 FIPS Series (4.x) | 3052 | +| YubiKey 5 Series (5.x) | 3052 | +| YubiKey 5 FIPS Series (5.x) | 3052 | -A NEO (pre-4.0), only has four slots, and will be able to hold four certs of the maximum -length. +> [!NOTE] +> The maximum allowable certificate size is determined by the YubiKey's APDU buffer size. For YubiKeys with firmware version 4.x and above, the buffer size is 3072. Certificates stored according to the PIV standard will have approximately 20 bytes of header data, including tag and length values, leaving 3052 bytes for the certificate itself. -| YubiKey Version | Maximum Total Cert
Space Available | Number of Certs
at Size | Number of Certs
at Maximum Size | -|:---------------------:|:--------------------------------------:|:---------------------------:|:-----------------------------------:| -| before 4.0 (e.g. NEO) | 8100 | 4 certs at 2025 bytes | 4 certs at 2025 bytes | -| 4.x | about 49,800 | 24 certs at 2075 bytes | 16 certs at 3052 bytes | -| 4.x FIPS | about 49,800 | 24 certs at 2075 bytes | 16 certs at 3052 bytes | -| 5.x | about 50,000 | 24 certs at 2084 bytes | 16 certs at 3052 bytes | -| 5.x FIPS | about 49,890 | 24 certs at 2079 bytes | 16 certs at 3052 bytes | - -Note that that total amount of storage on a YubiKey (for certs, for PUT DATA objects, -etc.) is about 51,000 bytes. Hence, if a YubiKey is loaded with 49,000 bytes of certs, -then there will be very little space left for anything else. +## Total space available for certificates -## Summary +Although YubiKeys with firmware version 4.x and higher will allow 3052-byte certificates, they will not be able to store 24 certificates of that size due to the YubiKey's total certificate space limit. Even if a YubiKey has empty certificate slots available, you cannot fill them once the maximum certificate space has been reached. -On a 5.x YubiKey, it is possible to store a 3,052-byte cert in a slot. If a cert is -bigger than 3,052 bytes, the YubiKey will reject it and the SDK will throw an exception. +However, a YubiKey NEO, which only has four slots, will be able to hold four certificates of the maximum length. -It is certainly possible to store several 3,052-byte certs on a 5.x YubiKey, but once the -total size limit is reached, the YubiKey won't be able to store any more, even if some of -the slots are empty. +| YubiKey Model
(and Firmware Version) | Maximum Total Certificate
Space Available | Maximum Average
Certificate Size | Number of Certificates
at Maximum Size | +|:----------------------------------------:|:---------------------------------------------:|:------------------------------------:|:------------------------------------------:| +| YubiKey NEO (prior to 4.x) | 8100 | 4 certs at 2025 bytes | 4 certs at 2025 bytes | +| YubiKey 4 Series (4.x) | about 49,800 | 24 certs at 2075 bytes | 16 certs at 3052 bytes | +| YubiKey 4 FIPS Series (4.x) | about 49,800 | 24 certs at 2075 bytes | 16 certs at 3052 bytes | +| YubiKey 5 Series (5.x) | about 50,000 | 24 certs at 2084 bytes | 16 certs at 3052 bytes | +| YubiKey 5 FIPS Series (5.x) | about 49,890 | 24 certs at 2079 bytes | 16 certs at 3052 bytes | -However, because a real world application will probably not use certs bigger than 2,000 -bytes, it is not likely it will ever run into a total space limitation and will be able -to store up to 24 certs. \ No newline at end of file +Note that the total amount of NVM (non-volatile memory) in a YubiKey's PIV application, which stores certificates (including the attestation certificate) *plus* [PIV data objects](xref:UsersManualPivObjects), is 51,200 bytes. Therefore, if a YubiKey is loaded with a lot of certificate data (for example, 49,000 bytes' worth), there will be very little space left for anything else. \ No newline at end of file diff --git a/docs/users-manual/getting-started/whats-new.md b/docs/users-manual/getting-started/whats-new.md index 2235d0b59..47daa4016 100644 --- a/docs/users-manual/getting-started/whats-new.md +++ b/docs/users-manual/getting-started/whats-new.md @@ -18,9 +18,24 @@ Here you can find all of the updates and release notes for published versions of ## 1.15.x Releases +### 1.15.1 + +Release date: January 28th, 2026 + +Bug Fixes: +- Fixed regression in Piv sample app ([#398](https://github.com/Yubico/Yubico.NET.SDK/pull/398) + +Documentation: + +- The documentation on [PIV certificate sizes](xref:UsersManualPivCertSizes) has been restructured to improve clarity and readability. Additionally, firmware behavior details and practical examples have been added/updated. ([#356](https://github.com/Yubico/Yubico.NET.SDK/pull/356)) + +Dependencies: + +- Several dependencies across integration, unit, and utilities test projects, the test app, and the Yubico.Core and Yubico.YubiKey projects have been updated. ([#388](https://github.com/Yubico/Yubico.NET.SDK/pull/388), [#387](https://github.com/Yubico/Yubico.NET.SDK/pull/387), [#386](https://github.com/Yubico/Yubico.NET.SDK/pull/386), [#384](https://github.com/Yubico/Yubico.NET.SDK/pull/384)) + ### 1.15.0 -Release date: December 3rd, 2025 +Release date: December 4th, 2025 Features: @@ -36,6 +51,11 @@ Bug Fixes: Documentation: +- Docs covering SDK support for the following FIDO2 extensions have been added to the User's Manual: + + - [third-party payment](xref:Fido2ThirdPartyPayment) ([#349](https://github.com/Yubico/Yubico.NET.SDK/pull/349)) + - [hmac-secret-mc](xref:Fido2HmacSecret) ([#350](https://github.com/Yubico/Yubico.NET.SDK/pull/350)) + - Comprehensive docs covering SDK support for the [Persistent PinUvAuthToken (PPUAT)](xref:Fido2AuthTokens#persistent-pinuvauthtoken-ppuat) have been added to the User's Manual. ([#333](https://github.com/Yubico/Yubico.NET.SDK/pull/333)) - NFC instructions have been added to the [FIDO2 reset](xref:Fido2Reset) docs. ([#341](https://github.com/Yubico/Yubico.NET.SDK/pull/341)) @@ -84,9 +104,9 @@ Features: - Support has been added for the following CTAP 2.2 and YubiKey firmware version 5.8 features ([#299](https://github.com/Yubico/Yubico.NET.SDK/pull/299)): - - [Persistent PinUvAuthToken (PPUAT)](xref:Fido2AuthTokens#persistent-pinuvauthtoken-ppuat): The [GetPersistentPinUvAuthToken()](xref:Yubico.YubiKey.Fido2.Fido2Session.GetPersistentPinUvAuthToken) method has been added to retrieve PPUATs for use with read-only FIDO2 credential management operations, including [EnumerateRelyingParties()](xref:Yubico.YubiKey.Fido2.Fido2Session.EnumerateRelyingParties), [EnumerateCredentialsForRelyingParty()](xref:Yubico.YubiKey.Fido2.Fido2Session.EnumerateCredentialsForRelyingParty%28Yubico.YubiKey.Fido2.RelyingParty%29), and [GetCredentialMetadata()](xref:Yubico.YubiKey.Fido2.Fido2Session.GetCredentialMetadata). PPUATs enable applications to list discoverable credentials from YubiKeys without requiring repeated PIN entry. + - [Persistent PinUvAuthToken (PPUAT)](xref:Fido2AuthTokens#persistent-pinuvauthtoken-ppuat): PPUATs are longer-lived auth tokens that can be used for read-only FIDO2 credential management operations, including [EnumerateRelyingParties()](xref:Yubico.YubiKey.Fido2.Fido2Session.EnumerateRelyingParties), [EnumerateCredentialsForRelyingParty()](xref:Yubico.YubiKey.Fido2.Fido2Session.EnumerateCredentialsForRelyingParty%28Yubico.YubiKey.Fido2.RelyingParty%29), and [GetCredentialMetadata()](xref:Yubico.YubiKey.Fido2.Fido2Session.GetCredentialMetadata). PPUATs enable applications to list discoverable credentials from YubiKeys without requiring repeated PIN entry. - - thirdPartyPayment extension: The [GetThirdPartyPaymentExtension](xref:Yubico.YubiKey.Fido2.AuthenticatorData.GetThirdPartyPaymentExtension) method has been added to check for and return the status of the thirdPartyPayment extension. The thirdPartyPayment extension enables YubiKeys to be used for cross-domain credentials without redirects, as required by Secure Payment Confirmation (SPC) workflows. + - [thirdPartyPayment extension](xref:Fido2ThirdPartyPayment): This extension enables YubiKeys to be used for payment authentication scenarios, including Secure Payment Confirmation (SPC) workflows, where the transaction initiator is not the relying party. - [hmac-secret-mc extension](xref:Fido2HmacSecret): This extension enables the retrieval of a symmetric secret during ``MakeCredential()``. The secret, which can be used for encryption/decryption, supports the use of PRF (Pseudo-Random Function) with YubiKeys. diff --git a/docs/users-manual/toc.yml b/docs/users-manual/toc.yml index a1cabdc1f..30e63c4d7 100644 --- a/docs/users-manual/toc.yml +++ b/docs/users-manual/toc.yml @@ -337,8 +337,12 @@ href: application-fido2/cred-blobs.md - name: Large blobs href: application-fido2/large-blobs.md - - name: HMAC secret extensions - href: application-fido2/hmac-secret.md + - name: Additional extensions + items: + - name: HMAC secret extensions + href: application-fido2/hmac-secret.md + - name: Third-party payment extension + href: application-fido2/thirdpartypayment.md - name: Commands items: - name: FIDO2 commands