diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..1dc5156 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,45 @@ +{ + "name": "Swift", + "image": "swift:6.0", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "false", + "username": "vscode", + "userUid": "1000", + "userGid": "1000", + "upgradePackages": "false" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "os-provided", + "ppa": "false" + }, + "ghcr.io/nikitakurpas/features/vapor-toolbox:1": {} + }, + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ], + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "lldb.library": "/usr/lib/liblldb.so" + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "sswg.swift-lang" + ] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "swift --version", + + // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b2f95bd --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @eu-digital-identity-wallet/niscy-admins \ No newline at end of file diff --git a/.github/workflows/build-package.yml b/.github/workflows/build-package.yml new file mode 100644 index 0000000..9a90171 --- /dev/null +++ b/.github/workflows/build-package.yml @@ -0,0 +1,22 @@ +--- +name: build-package +on: + pull_request: + types: [opened, reopened] + push: + branches: ['main'] + tags: [ v* ] +jobs: + build: + runs-on: "macos-14" + steps: + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '16.0' + - name: Get swift version + run: swift --version + - uses: actions/checkout@v4 + - name: Build + run: swift build + - name: Run tests + run: swift test \ No newline at end of file diff --git a/.github/workflows/dependencycheck.yml b/.github/workflows/dependencycheck.yml new file mode 100644 index 0000000..e5d3427 --- /dev/null +++ b/.github/workflows/dependencycheck.yml @@ -0,0 +1,15 @@ +name: SCA - Dependency-Check Caller +on: + push: + branches-ignore: + - 'dependabot/**' + workflow_dispatch: + +jobs: + + SCA_caller: + uses: eu-digital-identity-wallet/eudi-infra-ci/.github/workflows/sca.yml@main + secrets: + NVD_API_KEY: ${{ secrets.NVD_API_KEY }} + DOJO_TOKEN: ${{ secrets.DOJO_TOKEN }} + DOJO_URL: ${{ secrets.DOJO_URL }} diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml new file mode 100644 index 0000000..58ab8b1 --- /dev/null +++ b/.github/workflows/gitleaks.yml @@ -0,0 +1,14 @@ +name: Secret Scanning - Gitleaks Caller +on: + push: + branches-ignore: + - 'dependabot/**' + workflow_dispatch: + +jobs: + + Secret_Scanning_caller: + uses: eu-digital-identity-wallet/eudi-infra-ci/.github/workflows/secretscanning.yml@main + secrets: + DOJO_TOKEN: ${{ secrets.DOJO_TOKEN }} + DOJO_URL: ${{ secrets.DOJO_URL }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml new file mode 100644 index 0000000..1b70f75 --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,17 @@ +name: SAST - SonarCloud Caller +on: + push: + branches-ignore: + - 'dependabot/**' + pull_request_target: + workflow_dispatch: + +jobs: + + SAST_caller: + uses: eu-digital-identity-wallet/eudi-infra-ci/.github/workflows/sast_action.yml@main + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOJO_TOKEN: ${{ secrets.DOJO_TOKEN }} + DOJO_URL: ${{ secrets.DOJO_URL }} diff --git a/.gitignore b/.gitignore index 52fe2f7..4b23c2e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ playground.xcworkspace .build/ +# ignore test Xcode project +Tests/example_rqes/ + # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However @@ -40,7 +43,7 @@ playground.xcworkspace # Pods/ # # Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace +*.xcworkspace # Carthage # diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4f53d92 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,137 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported at [cnect-eudiw-development@ec.europa.eu](mailto:cnect-eudiw-development@ec.europa.eu). +to the community leaders responsible for enforcement. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org + +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html + +[Mozilla CoC]: https://github.com/mozilla/diversity + +[FAQ]: https://www.contributor-covenant.org/faq + +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..31d4b0c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,113 @@ +# Contribution Guidelines + +We welcome contributions to this project. To ensure that the process is smooth for everyone +involved, please follow the guidelines below. + +If you encounter a bug in the project, check if the bug has already been reported. If the +bug has not been reported, you can open an issue to report the bug. + +Before making any changes, it's a good practice to create an issue to describe the changes +you plan to make and the reasoning behind them. + +You can +read [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github) +for more information. + +## GitHub Flow + +We use the [GitHub Flow](https://guides.github.com/introduction/flow/) workflow for making +contributions to this project. This means that: + +1. Fork the repository and create a new branch from `main` for your changes. + + ```bash + git checkout main + git pull + git checkout -b my-branch + ``` + +2. Make changes to the code, documentation, or any other relevant files. +3. Commit your changes and push them to your forked repository. + + ```bash + git add . + git commit -m "Add a new feature" + git push origin my-branch + ``` + +4. Create a pull request from your branch to the `main` branch of this repository. + +## Pull Request Checklist + +* Branch from the main branch and, if needed, rebase to the current main branch before submitting + your pull request. If it doesn't merge cleanly with main you may be asked to rebase your changes. + +* Commits should be as small as possible while ensuring that each commit is correct independently ( + i.e., each commit should compile and pass tests). + +* Test your changes as thoroughly as possible before you commit them. Preferably, automate your test + by unit/integration tests. If tested manually, provide information about the test scope in the PR + description (e.g. “Test passed: Upgrade version from 0.42 to 0.42.23.”). + +* Create _Work In Progress [WIP]_ pull requests only if you need clarification or an explicit review + before you can continue your work item. + +* If your patch is not getting reviewed or you need a specific person to review it, you can @-reply + a reviewer asking for a review in the pull request or a comment. + +* Post review: + * If a review requires you to change your commit(s), please test the changes again. + * Amend the affected commit(s) and force push onto your branch. + * Set respective comments in your GitHub review to resolved. + * Create a general PR comment to notify the reviewers that your amendments are ready for another + round of review. + +## Branch Name Rules + +Please name your branch using the following convention: + +```text +/ +``` + +- `type` should be one of the following: + - `feat` for a new feature, + - `fix` for a bug fix, + - `docs` for documentation changes, + - `style` for changes that do not affect the code, such as formatting or whitespace, + - `refactor` for code refactoring, + - `test` for adding or updating tests, or + - `chore` for any other miscellaneous tasks. +- `short-description` should be a short, descriptive name of the changes you are making. + +For example: + +```text +feat/add-new-button +fix/typo-in-readme +docs/update-contributing-guide +style/format-code +refactor/extract-method +test/add-unit-tests +chore/update-dependencies +``` + +## Issues and Planning + +* We use GitHub issues to track bugs and enhancement requests. + +* Please provide as much context as possible when you open an issue. The information you provide + must be comprehensive enough to reproduce that issue for the assignee. Therefore, contributors may + use but aren't restricted to the issue template provided by the project maintainers. + +* When creating an issue, try using one of our issue templates which already contain some guidelines + on which content is expected to process the issue most efficiently. If no template applies, you + can of course also create an issue from scratch. + +* Please apply one or more applicable [labels](/../../labels) to your issue so that all community + members are able to cluster the issues better. + +## Code of Conduct + +Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). +By participating in this project, you agree to abide by its terms. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c8271c9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,132 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by + Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is + granting the License. + + "Legal Entity" shall mean the union of the acting entity and all other entities that control, are + controlled by, or are under common control with that entity. For the purposes of this definition, " + control" means (i) the power, direct or indirect, to cause the direction or management of such + entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this + License. + + "Source" form shall mean the preferred form for making modifications, including but not limited to + software source code, documentation source, and configuration files. + + "Object" form shall mean any form resulting from mechanical transformation or translation of a + Source form, including but not limited to compiled object code, generated documentation, and + conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or Object form, made available under the + License, as indicated by a copyright notice that is included in or attached to the work (an example + is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or + derived from) the Work and for which the editorial revisions, annotations, elaborations, or other + modifications represent, as a whole, an original work of authorship. For the purposes of this + License, Derivative Works shall not include works that remain separable from, or merely link (or + bind by name) to the interfaces of, the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including the original version of the Work and any + modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted + to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity + authorized to submit on behalf of the copyright owner. For the purposes of this definition, " + submitted" means any form of electronic, verbal, or written communication sent to the Licensor or + its representatives, including but not limited to communication on electronic mailing lists, source + code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor + for the purpose of discussing and improving the Work, but excluding communication that is + conspicuously marked or otherwise designated in writing by the copyright owner as "Not a + Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a + Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor + hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, + sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor + hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, + irrevocable (except as stated in this section) patent license to make, have made, use, offer to + sell, sell, import, and otherwise transfer the Work, where such license applies only to those + patent claims licensable by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) with the Work to which such + Contribution(s) was submitted. If You institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated + within the Work constitutes direct or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate as of the date such litigation is + filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof + in any medium, with or without modifications, and in Source or Object form, provided that You + meet the following conditions: + + You must give any other recipients of the Work or Derivative Works a copy of this License; and + You must cause any modified files to carry prominent notices stating that You changed the files; and + You must retain, in the Source form of any Derivative Works that You distribute, all copyright, + patent, trademark, and attribution notices from the Source form of the Work, excluding those notices + that do not pertain to any part of the Derivative Works; and + If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works + that You distribute must include a readable copy of the attribution notices contained within such + NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at + least one of the following places: within a NOTICE text file distributed as part of the Derivative + Works; within the Source form or documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and wherever such third-party notices + normally appear. The contents of the NOTICE file are for informational purposes only and do not + modify the License. You may add Your own attribution notices within Derivative Works that You + distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such + additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or + different license terms and conditions for use, reproduction, or distribution of Your modifications, + or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of + the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution + intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms + and conditions of this License, without any additional terms or conditions. Notwithstanding the + above, nothing herein shall supersede or modify the terms of any separate license agreement you + may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service + marks, or product names of the Licensor, except as required for reasonable and customary use in + describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor + provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, + any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or + redistributing the Work and assume any risks associated with Your exercise of permissions under + this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including + negligence), contract, or otherwise, unless required by applicable law (such as deliberate and + grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for + damages, including any direct, indirect, special, incidental, or consequential damages of any + character arising as a result of this License or out of the use or inability to use the Work ( + including but not limited to damages for loss of goodwill, work stoppage, computer failure or + malfunction, or any and all other commercial damages or losses), even if such Contributor has + been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works + thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, + indemnity, or other liability obligations and/or rights consistent with this License. However, in + accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, + not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each + Contributor harmless for any liability incurred by, or claims asserted against, such Contributor + by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..560a6fc --- /dev/null +++ b/Package.resolved @@ -0,0 +1,51 @@ +{ + "originHash" : "67d6e755c4890cdfcb60930d0b952815385f42101d98ff3d40456723c03c6c74", + "pins" : [ + { + "identity" : "eudi-lib-ios-rqes-csc-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-rqes-csc-swift.git", + "state" : { + "revision" : "d286db938dd6bddcd2aa22051e54211227cf5cba", + "version" : "0.0.1" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "1fbb6ef21f1525ed5faf4c95207b9c11bea27e94", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "06dc63c6d8da54ee11ceb268cde1fa68161afc96", + "version" : "3.9.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..52c3403 --- /dev/null +++ b/Package.swift @@ -0,0 +1,36 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "RqesKit", + defaultLocalization: "en", + platforms: [.iOS(.v14), .macOS(.v12)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "RqesKit", + targets: ["RqesKit"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-rqes-csc-swift.git", from: "0.0.1"), + .package(url: "https://github.com/apple/swift-certificates.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "RqesKit", dependencies: [ + .product(name: "RQES_LIBRARY", package: "eudi-lib-ios-rqes-csc-swift"), + .product(name: "X509", package: "swift-certificates"), + .product(name: "Logging", package: "swift-log") + ]), + .testTarget( + name: "RqesKitTests", + dependencies: ["RqesKit"] + ), + ] +) diff --git a/README.md b/README.md index 31c5c4c..47cbd03 100644 --- a/README.md +++ b/README.md @@ -1 +1,151 @@ -# eudi-lib-ios-rqes-kit \ No newline at end of file +# EUDI Remote Qualified Electronic Signature (RQES) Kit library for iOS + +:heavy_exclamation_mark: **Important!** Before you proceed, please read +the [EUDI Wallet Reference Implementation project description](https://github.com/eu-digital-identity-wallet/.github/blob/main/profile/reference-implementation.md) + +## Overview + +This module provides the core functionality for the EUDI Wallet, focusing on the Remote Qualified +Electronic Signature (RQES) service. The `RQESService` interface defines methods for obtaining +credential authorization URLs, authorizing credentials, and signing documents. It ensures secure and +compliant electronic signatures by interacting with authorized credentials and handling document +signing processes. + +## Installation +To use RQES Kit, add the following dependency to your Package.swift: +```swift +dependencies: [ + .package(url: "https://github.com/niscy-eudiw/eudi-lib-ios-rqes-kit", branch: "initial") +] +``` + +Then add the Eudi Wallet package to your target's dependencies: +```swift +dependencies: [ + .product(name: "RqesKit", package: "eudi-lib-ios-rqes-kit"), +] +``` + +## Document signing flow + +```mermaid +sequenceDiagram + participant Client + participant RQESService + participant RQESServiceAuthorized + participant RQESServiceCredentialAuthorized + Client ->>+ RQESService: getRSSPMetadata() + RQESService -->>- Client: RSSPMetadata + Client ->>+ RQESService: getServiceAuthorizationUrl() + RQESService -->>- Client: URL + Client ->>+ RQESService: authorizeService(authorizationCode) + RQESService -->>- Client: RQESServiceAuthorized + Client ->>+ RQESServiceAuthorized: getCredentialsList(request) + RQESServiceAuthorized -->>- Client: List + Client ->>+ RQESServiceAuthorized: getCredentialAuthorizationUrl(credential, documents) + RQESServiceAuthorized -->>- Client: URL + Client ->>+ RQESServiceAuthorized: authorizeCredential(authorizationCode) + RQESServiceAuthorized -->>- Client: RQESServiceCredentialAuthorized + Client ->>+ RQESServiceCredentialAuthorized: signDocuments(algorithmOID) + RQESServiceCredentialAuthorized -->>- Client: SignedDocuments +``` + +## How to use + +At first, construct an instance of the `RQESService` like shown below: + +```swift +let cscClientConfig = CSCClientConfig( + OAuth2Client: CSCClientConfig.OAuth2Client( + clientId: "wallet-client", + clientSecret: "somesecret2" + ), + authFlowRedirectionURI: "https://oauthdebugger.com/debug", + scaBaseURL: "https://walletcentric.signer.eudiw.dev" +) +var rqesService = RQESService( + clientConfig: cscClientConfig, + defaultHashAlgorithmOID: .SHA256, + defaultSigningAlgorithmOID: .RSA +) +``` + +You can get the metadata of the RQES service by calling the `getRSSPMetadata` method: + +```swift +let metadata = try await rqesService.getRSSPMetadata() +``` + +To authorize the service, you need to get the authorization URL and open it in a browser. After the +user has authorized the service, the browser will be redirected to the `redirectUri`, +that +is configured in the `CSCClientConfig`, with a query parameter named `code` containing the +authorization code. You can then authorize the service by calling the `authorizeService` method: + +```swift +let authorizationUrl = try await rqesService.getServiceAuthorizationUrl() + +// Open the authorizationUrl in a browser +// After the user has authorized the service, the browser will be redirected to the redirectUri +// with a query parameter named "code" containing the authorization code + +let authorizedService = try await rqesService.authorizeService(authorizationCode) +``` + +With the authorized service, you can list the available credentials by calling the `getCredentialsList` +method. + +You can then select the credential you want to use, prepare the documents to sign, and get +the credential authorization URL by calling the `getCredentialAuthorizationUrl` method. After the +user has authorized the credential, you can authorize it by calling the `authorizeCredential` +method. + +Finally, you can sign the documents by calling the `signDocuments` method. + +```swift +let credentials = try await authorizedService.getCredentialsList() +// Use the credentials to select the one you want to use +// For example, select the first credential + +let credential = credentials.first! +// Prepare the documents to sign +let unsignedDocuments = [Document(label: "Document to sign", fileURL: Bundle.main.url(forResource: "document", withExtension:"pdf")))] + +// Get the credential authorization URL for the selected credential and documents +let credentialAuthorizationUrl = try await authorizedService.getCredentialAuthorizationUrl( + credentialInfo: credential, + documents: unsignedDocuments, +) + +// Use the credentialAuthorizationUrl to open a browser and let the user authorize the credential +// and get the authorization code from the redirect URI query parameter + +// Authorize the credential +let authorizedCredential = try await authorizedService.authorizeCredential(authorizationCode) + +// Sign the documents +let signAlgorithm = SigningAlgorithmOID.ECDSA_SHA256 +let signedDocuments = try await authorizedCredential.signDocuments(signAlgorithmOID: signAlgorithm) +``` + +## How to contribute + +We welcome contributions to this project. To ensure that the process is smooth for everyone +involved, follow the guidelines found in [CONTRIBUTING.md](CONTRIBUTING.md). + + +### License details + +Copyright (c) 2023 European Commission + +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. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..cc207ab --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,42 @@ +# EU Digital Identity Wallet Vulnerability Disclosure Policy (VDP) + +At the European Commission, we treat the security of our Communication and Information Systems as a top priority, in line with Commission Decision EC 2017/46. However, vulnerabilities can never be completely eliminated, despite all efforts. If exploited, such vulnerabilities can harm the confidentiality, integrity or availability of the Commission's systems and of the information processed therein. To identify and remediate vulnerabilities as soon as possible, we value the input of external entities acting in good faith, and we encourage responsible vulnerability research and disclosure. This document sets out our definition of good faith in the context of finding and reporting vulnerabilities, as well as what you can expect from us in return. + +## Scope + +- Architecture and Reference Framework +- Source code in [eu-digital-identity-wallet](https://github.com/eu-digital-identity-wallet) public repositories + +## If you have identified a vulnerability, please do the following + +- E-mail your findings to , specifying whether or not you agree to your name or pseudonym being made publicly available as the discoverer of the problem. +- Encrypt your findings using our [PGP key](https://ec.europa.eu/assets/digit/pgpkey/ec-vulnerability-disclosure-pgp.txt) to prevent this critical information from falling into the wrong hands. +- Provide us with sufficient information to reproduce the problem so that we can resolve it as quickly as possible. Usually, the IP address or the URL of the affected system and a description of the vulnerability will be sufficient, but complex vulnerabilities may require further explanation in terms of technical information or potential proof-of-concept code. +- Provide your report in English, preferably, or in any other official language of the European Union. +- Inform us if you agree to make your name/pseudonym publicly available as the discoverer of the vulnerability. + +## Please do not do the following + +- Do not take advantage of the vulnerability or problem you have discovered, for example, by downloading more data than necessary to demonstrate the vulnerability, deleting, or modifying other people’s data. +- Do not reveal any data downloaded during the discovery to any other parties. +- Do not reveal the problem to others until it has been resolved. +- Do not perform the following actions: + - Placing malware (virus, worm, Trojan horse, etc.) within the system. + - Reading, copying, modifying or deleting data from the system. + - Making changes to the system. + - Repeatedly accessing the system or sharing access with others. + - Using any access obtained to attempt to access other systems. + - Changing access rights for any other users. + - Using automated scanning tools. + - Using the so-called "brute force" of access to the system. + - Using denial-of-service or social engineering (phishing, vishing, spam, etc.). +- Do not use attacks on physical security. + +## What we promise + +- We will respond to your report within three business days with our evaluation of the report. + +- We will handle your report with strict confidentiality. +- Where possible, we will inform you when the vulnerability has been remedied. +- We will process the personal data that you provide (such as your e-mail address and name) in accordance with the applicable data protection legislation and will not pass on your personal details to third parties without your permission. +- In the public information concerning the problem reported, we will publish your name as the discoverer of the problem if you have agreed to this in your initial e-mail diff --git a/Sources/RqesKit/Document.swift b/Sources/RqesKit/Document.swift new file mode 100644 index 0000000..5db5e4a --- /dev/null +++ b/Sources/RqesKit/Document.swift @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 European Commission + * + * 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. + */ +import Foundation + +/// Represents a document. +public struct Document { + public init(id: String, fileURL: URL) { + self.id = id + self.fileURL = fileURL + } + /// The identifier of the document. + public let id: String + /// The file URL of the document. + public let fileURL: URL + +} diff --git a/Sources/RqesKit/JSONUtils.swift b/Sources/RqesKit/JSONUtils.swift new file mode 100644 index 0000000..d7e8efe --- /dev/null +++ b/Sources/RqesKit/JSONUtils.swift @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 European Commission + * + * 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. + */ +import Foundation + +struct JSONUtils { + + public static func stringify(_ object: T) -> String? { + let encoder = JSONEncoder() + encoder.outputFormatting = .withoutEscapingSlashes + do { + let jsonData = try encoder.encode(object) + return String(data: jsonData, encoding: .utf8) + } catch { + print("Error encoding object to JSON: \(error)") + return nil + } + } +} diff --git a/Sources/RqesKit/RQESService.swift b/Sources/RqesKit/RQESService.swift new file mode 100644 index 0000000..96dddc8 --- /dev/null +++ b/Sources/RqesKit/RQESService.swift @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2023 European Commission + * + * 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. + */ + +import Foundation +import RQES_LIBRARY +import CommonCrypto +import X509 +import SwiftASN1 + +public typealias CSCClientConfig = RQES_LIBRARY.CSCClientConfig +public typealias RSSPMetadata = RQES_LIBRARY.InfoServiceResponse +public typealias CredentialInfo = CSCCredentialsListResponse.CredentialInfo +public typealias HashAlgorithmOID = RQES_LIBRARY.HashAlgorithmOID + + // --------------------------- +public class RQESService: RQESServiceProtocol, @unchecked Sendable { + + var baseProviderUrl: String? + var clientConfig: CSCClientConfig + var state: String? + var rqes: RQES! + var defaultHashAlgorithmOID: HashAlgorithmOID + var defaultSigningAlgorithmOID: SigningAlgorithmOID + + /// Initialize the RQES service + /// - Parameter clientConfig: CSC client configuration + required public init(clientConfig: CSCClientConfig, defaultHashAlgorithmOID: HashAlgorithmOID = .SHA256, defaultSigningAlgorithmOID: SigningAlgorithmOID = .RSA) { + self.clientConfig = clientConfig + self.defaultHashAlgorithmOID = defaultHashAlgorithmOID + self.defaultSigningAlgorithmOID = defaultSigningAlgorithmOID + } + + /// Retrieve the RSSP metadata + public func getRSSPMetadata() async throws -> RSSPMetadata { + // STEP 1: Initialize an instance of RQES to access library services + // This initializes the RQES object for invoking various service methods + rqes = await RQES(cscClientConfig: clientConfig) + // STEP 2: Retrieve service information using the InfoService + let request = InfoServiceRequest(lang: "en-US") + let response = try await rqes.getInfo(request: request) + baseProviderUrl = response.oauth2 + return response + } + + /// Retrieve the service authorization URL + /// - Returns: The service authorization URL + /// The service authorization URL is used to authorize the service to access the user's credentials. + public func getServiceAuthorizationUrl() async throws -> URL { + state = UUID().uuidString + // STEP 5: Set up an authorization request using OAuth2AuthorizeRequest with required parameters + let response = try await rqes.prepareServiceAuthorizationRequest(walletState: state!) + return URL(string: response.authorizationCodeURL)! + } + + /// Authorize the service + /// - Parameter authorizationCode: The authorization code + /// - Returns: The authorized service instance + /// Once the authorizationCode is obtained using the service authorization URL, it can be used to authorize the service. + public func authorizeService(authorizationCode: String) async throws -> RQESServiceAuthorized { + // STEP 6: Request an OAuth2 Token using the authorization code + let tokenRequest = OAuth2TokenDto(code: authorizationCode, state: state!) + let tokenResponse = try await rqes.getOAuth2Token(request: tokenRequest) + let accessToken = tokenResponse.accessToken + return RQESServiceAuthorized(rqes, clientConfig: self.clientConfig, defaultHashAlgorithmOID: defaultHashAlgorithmOID, defaultSigningAlgorithmOID: defaultSigningAlgorithmOID, state: state!, accessToken: accessToken, baseProviderUrl: baseProviderUrl!) + } + + + // MARK: - Utils + static func calculateHashes(_ rqes: RQES, documents: [URL], certificates: [String], accessToken: String, hashAlgorithmOID: HashAlgorithmOID, signatureFormat: SignatureFormat = SignatureFormat.P, conformanceLevel: ConformanceLevel = ConformanceLevel.ADES_B_B, signedEnvelopeProperty: SignedEnvelopeProperty = SignedEnvelopeProperty.ENVELOPED) async throws -> CalculateHashResponse { + let request = CalculateHashRequest( + documents: documents.map { CalculateHashRequest.Document(document: (try! Data(contentsOf: $0)).base64EncodedString(), signatureFormat: signatureFormat, conformanceLevel: conformanceLevel, signedEnvelopeProperty: SignedEnvelopeProperty.ENVELOPED, container: "No") }, endEntityCertificate: certificates[0], certificateChain: Array(certificates.dropFirst()), hashAlgorithmOID: hashAlgorithmOID) + return try await rqes.calculateHash(request: request, accessToken: accessToken) + } + + static func saveToTempFile(data: Data) throws -> URL { + let tempDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + let tempFile = tempDir.appendingPathComponent(UUID().uuidString) + try data.write(to: tempFile) + return tempFile + } + +} + +extension Data { + enum Algorithm { + case sha256 + + var digestLength: Int { + switch self { + case .sha256: return Int(CC_SHA256_DIGEST_LENGTH) + } + } + } + + func hash(for algorithm: Algorithm) -> Data { + let hashBytes = UnsafeMutablePointer.allocate(capacity: algorithm.digestLength) + defer { hashBytes.deallocate() } + switch algorithm { + case .sha256: + withUnsafeBytes { (buffer) -> Void in + CC_SHA256(buffer.baseAddress!, CC_LONG(buffer.count), hashBytes) + } + } + + return Data(bytes: hashBytes, count: algorithm.digestLength) + } +} + +extension X509.Certificate { + var base64String: String { + var ser = DER.Serializer() + try! serialize(into: &ser) + return Data(ser.serializedBytes).base64EncodedString() + } +} diff --git a/Sources/RqesKit/RQESServiceAuthorized.swift b/Sources/RqesKit/RQESServiceAuthorized.swift new file mode 100644 index 0000000..7ba4b8e --- /dev/null +++ b/Sources/RqesKit/RQESServiceAuthorized.swift @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023 European Commission + * + * 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. + */ + +import Foundation +import RQES_LIBRARY +import CommonCrypto +import X509 +import SwiftASN1 + +public class RQESServiceAuthorized: RQESServiceAuthorizedProtocol, @unchecked Sendable { + + var rqes: RQES + var clientConfig: CSCClientConfig + var accessToken: String + var calculateHashResponse: CalculateHashResponse? + var documents: [Document]? + var state: String + var credentialInfo: CredentialInfo? + var authorizationDetailsJsonString: String? + var hashAlgorithmOID: HashAlgorithmOID? + var defaultHashAlgorithmOID: HashAlgorithmOID + var defaultSigningAlgorithmOID: SigningAlgorithmOID + + public init(_ rqes: RQES, clientConfig: CSCClientConfig, defaultHashAlgorithmOID: HashAlgorithmOID, defaultSigningAlgorithmOID: SigningAlgorithmOID, state: String, accessToken: String, baseProviderUrl: String) { + self.rqes = rqes + self.clientConfig = clientConfig + self.defaultHashAlgorithmOID = defaultHashAlgorithmOID + self.defaultSigningAlgorithmOID = defaultSigningAlgorithmOID + self.state = state + self.accessToken = accessToken + } + + /// Retrieve the list of credentials + /// - Returns: The list of credentials + /// The credentials are the user's credentials that can be used to sign the documents. + public func getCredentialsList() async throws -> [CredentialInfo] { + // STEP 7: Request the list of credentials using the access token + let requestDefault = CSCCredentialsListRequest(credentialInfo: true, certificates: "chain", certInfo: true) + let response = try await rqes.getCredentialsList(request: requestDefault, accessToken: accessToken) + guard let credentialInfos = response.credentialInfos else { throw OAuth2TokenError.invalidResponse } + return credentialInfos + } + + /// Get the credential authorization URL + /// - Parameters: + /// - credentialInfo: Information about the credential. + /// - documents: An array of documents that will be signed. + /// - hashAlgorithmOID: The object identifier (OID) of the hash algorithm to be used, optional. + /// - certificates: An optional array of X509 certificates. + /// - Returns: The credential authorization URL + /// The credential authorization URL is used to authorize the credential that will be used to sign the documents. + public func getCredentialAuthorizationUrl(credentialInfo: CredentialInfo, documents: [Document], hashAlgorithmOID: HashAlgorithmOID? = nil, certificates: [X509.Certificate]? = nil) async throws -> URL { + self.documents = documents + self.credentialInfo = credentialInfo + self.hashAlgorithmOID = hashAlgorithmOID ?? defaultHashAlgorithmOID + let certs = certificates?.map(\.base64String) ?? credentialInfo.cert.certificates + // STEP 9: calculate hashes + calculateHashResponse = try await RQESService.calculateHashes(rqes, documents: documents.map(\.fileURL), certificates: certs, accessToken: accessToken, hashAlgorithmOID: self.hashAlgorithmOID!) + // STEP 10: Set up an credential authorization request using OAuth2AuthorizeRequest with required parameters + let authorizationDetails = AuthorizationDetails([ + AuthorizationDetailsItem(documentDigests: calculateHashResponse!.hashes.enumerated().map { i,h in DocumentDigest(label: documents[i].id, hash: h) }, credentialID: credentialInfo.credentialID, hashAlgorithmOID: self.hashAlgorithmOID!, locations: [], type: "credential") ]) + authorizationDetailsJsonString = JSONUtils.stringify(authorizationDetails) + let credentialResponse = try await rqes.prepareCredentialAuthorizationRequest(walletState: state, authorizationDetails: authorizationDetailsJsonString!) + return URL(string: credentialResponse.authorizationCodeURL)! + } + + /// Authorizes a credential using the provided authorization code. + /// - Parameter authorizationCode: A `String` containing the authorization code required for credential authorization. + /// - Returns: An instance of `RQESServiceCredentialAuthorized` upon successful authorization. + /// Once the authorizationCode is obtained using the credential authorization URL, it can be used to authorize the credential. The authorized credential can be used to sign the documents. + public func authorizeCredential(authorizationCode: String) async throws -> RQESServiceCredentialAuthorized { + // STEP 11: Request OAuth2 token for credential authorization + let tokenCredentialRequest = OAuth2TokenDto(code: authorizationCode, state: state, authorizationDetails: authorizationDetailsJsonString) + let tokenCredentialResponse = try await rqes.getOAuth2Token(request: tokenCredentialRequest) + let credentialAccessToken = tokenCredentialResponse.accessToken + return RQESServiceCredentialAuthorized(rqes: rqes, clientConfig: clientConfig, credentialInfo: credentialInfo!, credentialAccessToken: credentialAccessToken, documents: documents!, calculateHashResponse: calculateHashResponse!, hashAlgorithmOID: hashAlgorithmOID!, defaultSigningAlgorithmOID: defaultSigningAlgorithmOID) + } +} diff --git a/Sources/RqesKit/RQESServiceCredentialAuthorized.swift b/Sources/RqesKit/RQESServiceCredentialAuthorized.swift new file mode 100644 index 0000000..7eab9d5 --- /dev/null +++ b/Sources/RqesKit/RQESServiceCredentialAuthorized.swift @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 European Commission + * + * 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. + */ + +import Foundation +import RQES_LIBRARY +import CommonCrypto +import X509 +import SwiftASN1 + +public typealias SigningAlgorithmOID = RQES_LIBRARY.SigningAlgorithmOID +/// The authorized credential is used to sign the documents. The list of documents that will be signed using the authorized credential are the documents +/// that were passed to the ``RQESServiceAuthorizedProtocol.getCredentialAuthorizationUrl`` method. +public class RQESServiceCredentialAuthorized: RQESServiceCredentialAuthorizedProtocol, @unchecked Sendable { + var rqes: RQES + var clientConfig: CSCClientConfig + var credentialInfo: CredentialInfo + var credentialAccessToken: String + var documents: [Document] + var calculateHashResponse: CalculateHashResponse + var hashAlgorithmOID: HashAlgorithmOID + var defaultSigningAlgorithmOID: SigningAlgorithmOID + + public init(rqes: RQES, clientConfig: CSCClientConfig, credentialInfo: CredentialInfo, credentialAccessToken: String, documents: [Document], calculateHashResponse: CalculateHashResponse, hashAlgorithmOID: HashAlgorithmOID, defaultSigningAlgorithmOID: SigningAlgorithmOID) { + self.rqes = rqes + self.clientConfig = clientConfig + self.credentialInfo = credentialInfo + self.credentialAccessToken = credentialAccessToken + self.documents = documents + self.calculateHashResponse = calculateHashResponse + self.hashAlgorithmOID = hashAlgorithmOID + self.defaultSigningAlgorithmOID = defaultSigningAlgorithmOID + } + + /// Signs the documents using the specified hash algorithm and certificates. + /// + /// - Parameters: + /// - signAlgorithmOID: The object identifier (OID) of the algorithm to be used for signing. This parameter is optional. + /// - certificates: An array of X509 certificates to be used for signing. This parameter is optional. + /// + /// - Returns: An array of signed documents. + /// + /// The list of documents that will be signed using the authorized credential are the documents + /// that were passed to the ``RQESServiceAuthorizedProtocol.getCredentialAuthorizationUrl`` method. + public func signDocuments(signAlgorithmOID: SigningAlgorithmOID? = nil, certificates: [X509.Certificate]? = nil) async throws -> [Document] { + // STEP 12: Sign the calculated hash with the credential + let signHashRequest = SignHashRequest(credentialID: credentialInfo.credentialID, hashes: calculateHashResponse.hashes, hashAlgorithmOID: hashAlgorithmOID, signAlgo: signAlgorithmOID ?? defaultSigningAlgorithmOID, operationMode: "S") + let signHashResponse = try await rqes.signHash(request: signHashRequest, accessToken: credentialAccessToken) + let certs = certificates?.map(\.base64String) ?? credentialInfo.cert.certificates + // STEP 13: Obtain the signed document + let obtainSignedDocRequest = ObtainSignedDocRequest(documents: documents.map { ObtainSignedDocRequest.Document( + document: (try! Data(contentsOf: $0.fileURL)).base64EncodedString(), signatureFormat: SignatureFormat.P, conformanceLevel: ConformanceLevel.ADES_B_B, signedEnvelopeProperty: SignedEnvelopeProperty.ENVELOPED, container: "No") }, + endEntityCertificate: certs.first!, certificateChain: Array(certs.dropFirst()), hashAlgorithmOID: hashAlgorithmOID, date: calculateHashResponse.signatureDate, signatures: signHashResponse.signatures ?? []) + let obtainSignedDocResponse = try await rqes.obtainSignedDoc(request: obtainSignedDocRequest, accessToken: credentialAccessToken) + + let documentsWithSignature = obtainSignedDocResponse.documentWithSignature.enumerated().map { i, d in Document(id: documents[i].id, fileURL: try! RQESService.saveToTempFile(data: Data(base64Encoded: d)!)) } + return documentsWithSignature + } + +} \ No newline at end of file diff --git a/Sources/RqesKit/RqesKit.swift b/Sources/RqesKit/RqesKit.swift new file mode 100644 index 0000000..101a6b5 --- /dev/null +++ b/Sources/RqesKit/RqesKit.swift @@ -0,0 +1,5 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book +import Logging + +let logger = Logger(label: "RqesKit") \ No newline at end of file diff --git a/Sources/RqesKit/RqesProtocols.swift b/Sources/RqesKit/RqesProtocols.swift new file mode 100644 index 0000000..8e06384 --- /dev/null +++ b/Sources/RqesKit/RqesProtocols.swift @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 European Commission + * + * 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. + */ + +import Foundation +import RQES_LIBRARY +import CommonCrypto +import X509 +import SwiftASN1 + +/// The RQES service protocol +public protocol RQESServiceProtocol { + associatedtype RQESServiceAuthorizedImpl: RQESServiceAuthorizedProtocol + /// Initialize the RQES service + /// - Parameter clientConfig: CSC client configuration + init(clientConfig: CSCClientConfig, defaultHashAlgorithmOID: HashAlgorithmOID, defaultSigningAlgorithmOID: SigningAlgorithmOID) + /// Retrieve the RSSP metadata + func getRSSPMetadata() async throws -> RSSPMetadata + /// Retrieve the service authorization URL + /// - Returns: The service authorization URL + /// The service authorization URL is used to authorize the service to access the user's credentials. + func getServiceAuthorizationUrl() async throws -> URL + /// Authorize the service + /// - Parameter authorizationCode: The authorization code + /// - Returns: The authorized service instance + /// Once the authorizationCode is obtained using the service authorization URL, it can be used to authorize the service. + func authorizeService(authorizationCode: String) async throws -> RQESServiceAuthorizedImpl +} + +/// The authorized service protocol. +/// The authorized service is used to access the user's credentials +public protocol RQESServiceAuthorizedProtocol { + associatedtype RQESServiceCredentialAuthorizedImpl: RQESServiceCredentialAuthorizedProtocol + /// Retrieve the list of credentials + /// - Returns: The list of credentials + /// The credentials are the user's credentials that can be used to sign the documents. + func getCredentialsList() async throws -> [CredentialInfo] + /// Get the credential authorization URL + /// - Parameters: + /// - credentialInfo: Information about the credential. + /// - documents: An array of documents that will be signed. + /// - hashAlgorithmOID: The object identifier (OID) of the hash algorithm to be used, optinal. + /// - certificates: An optional array of X509 certificates. + /// - Returns: The credential authorization URL + /// The credential authorization URL is used to authorize the credential that will be used to sign the documents. + func getCredentialAuthorizationUrl(credentialInfo: CredentialInfo, documents: [Document], hashAlgorithmOID: HashAlgorithmOID?, certificates: [X509.Certificate]?) async throws -> URL + /// Authorizes a credential using the provided authorization code. + /// - Parameter authorizationCode: A `String` containing the authorization code required for credential authorization. + /// - Returns: An instance of `RQESServiceCredentialAuthorizedImpl` upon successful authorization. + /// Once the authorizationCode is obtained using the credential authorization URL, it can be used to authorize the credential. The authorized credential can be used to sign the documents. + func authorizeCredential(authorizationCode: String) async throws -> RQESServiceCredentialAuthorizedImpl +} + +/// The credential authorized protocol. + +/// The authorized credential is used to sign the documents. The list of documents that will be signed using the authorized credential are the documents +/// that were passed to the ``RQESServiceAuthorizedProtocol.getCredentialAuthorizationUrl`` method. +public protocol RQESServiceCredentialAuthorizedProtocol { + /// Signs the documents using the specified hash algorithm and certificates. + /// + /// - Parameters: + /// - signAlgorithmOID: The object identifier (OID) of the algorithm to be used for signing. This parameter is optional. + /// - certificates: An array of X509 certificates to be used for signing. This parameter is optional. + /// + /// - Returns: An array of signed documents. + /// + /// The list of documents that will be signed using the authorized credential are the documents + /// that were passed to the ``RQESServiceAuthorizedProtocol.getCredentialAuthorizationUrl`` method. + func signDocuments(signAlgorithmOID: SigningAlgorithmOID?, certificates: [X509.Certificate]?) async throws -> [Document] +} \ No newline at end of file diff --git a/Tests/RqesKitTests/RqesKitTests.swift b/Tests/RqesKitTests/RqesKitTests.swift new file mode 100644 index 0000000..a79793e --- /dev/null +++ b/Tests/RqesKitTests/RqesKitTests.swift @@ -0,0 +1,15 @@ +import Foundation +import Testing +import X509 +import SwiftASN1 +@testable import RqesKit + +@Test func makeCertX509example() async throws { + let certBase64 = "MIIDHTCCAqOgAwIBAgIUVqjgtJqf4hUYJkqdYzi+0xwhwFYwCgYIKoZIzj0EAwMwXDEeMBwGA1UEAwwVUElEIElzc3VlciBDQSAtIFVUIDAxMS0wKwYDVQQKDCRFVURJIFdhbGxldCBSZWZlcmVuY2UgSW1wbGVtZW50YXRpb24xCzAJBgNVBAYTAlVUMB4XDTIzMDkwMTE4MzQxN1oXDTMyMTEyNzE4MzQxNlowXDEeMBwGA1UEAwwVUElEIElzc3VlciBDQSAtIFVUIDAxMS0wKwYDVQQKDCRFVURJIFdhbGxldCBSZWZlcmVuY2UgSW1wbGVtZW50YXRpb24xCzAJBgNVBAYTAlVUMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEFg5Shfsxp5R/UFIEKS3L27dwnFhnjSgUh2btKOQEnfb3doyeqMAvBtUMlClhsF3uefKinCw08NB31rwC+dtj6X/LE3n2C9jROIUN8PrnlLS5Qs4Rs4ZU5OIgztoaO8G9o4IBJDCCASAwEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBSzbLiRFxzXpBpmMYdC4YvAQMyVGzAWBgNVHSUBAf8EDDAKBggrgQICAAABBzBDBgNVHR8EPDA6MDigNqA0hjJodHRwczovL3ByZXByb2QucGtpLmV1ZGl3LmRldi9jcmwvcGlkX0NBX1VUXzAxLmNybDAdBgNVHQ4EFgQUs2y4kRcc16QaZjGHQuGLwEDMlRswDgYDVR0PAQH/BAQDAgEGMF0GA1UdEgRWMFSGUmh0dHBzOi8vZ2l0aHViLmNvbS9ldS1kaWdpdGFsLWlkZW50aXR5LXdhbGxldC9hcmNoaXRlY3R1cmUtYW5kLXJlZmVyZW5jZS1mcmFtZXdvcmswCgYIKoZIzj0EAwMDaAAwZQIwaXUA3j++xl/tdD76tXEWCikfM1CaRz4vzBC7NS0wCdItKiz6HZeV8EPtNCnsfKpNAjEAqrdeKDnr5Kwf8BA7tATehxNlOV4Hnc10XO1XULtigCwb49RpkqlS2Hul+DpqObUs" + let certData = [UInt8](Data(base64Encoded: certBase64)!) + let cert = try X509.Certificate(derEncoded: certData) + var ser = DER.Serializer() + try cert.serialize(into: &ser) + #expect(ser.serializedBytes == certData) + #expect(Data(ser.serializedBytes).base64EncodedString() == certBase64) +}