├── .gitignore ├── generated-certs └── .gitignore ├── validator ├── requirements.dev.txt ├── requirements.txt ├── README.md ├── Dockerfile └── validator.py ├── .github ├── dependabot.yml ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md ├── private.env.example ├── public.env.example ├── validate-crt-with-docker.sh ├── gencert-with-docker.sh ├── LICENSE ├── docker-entrypoint.sh ├── Dockerfile ├── CODE_OF_CONDUCT.md ├── gencert-private.sh ├── gencert-idp.sh ├── README.md └── gencert-public.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.crt 2 | *.key 3 | -------------------------------------------------------------------------------- /generated-certs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /validator/requirements.dev.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | isort 3 | -------------------------------------------------------------------------------- /validator/requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography==3.4.6 2 | -------------------------------------------------------------------------------- /validator/README.md: -------------------------------------------------------------------------------- 1 | # Validator 2 | 3 | A script to check the certificate specifications. 4 | 5 | $ pip install -r requirements.txt 6 | $ CERT_FILE=/path/to/cert/file.pem validator.py 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - 5 | package-ecosystem: github-actions 6 | directory: / 7 | schedule: 8 | interval: daily 9 | -------------------------------------------------------------------------------- /private.env.example: -------------------------------------------------------------------------------- 1 | COMMON_NAME=Comune di Roma 2 | ENTITY_ID=https://spid.comune.roma.it/metadata 3 | KEY_LEN=3072 4 | LOCALITY_NAME=Roma 5 | MD_ALG=sha256 6 | ORGANIZATION_IDENTIFIER=VATIT-02438750586 7 | ORGANIZATION_NAME=Comune di Roma 8 | SPID_SECTOR=private 9 | -------------------------------------------------------------------------------- /public.env.example: -------------------------------------------------------------------------------- 1 | COMMON_NAME=Comune di Roma 2 | DAYS=3650 3 | ENTITY_ID=https://spid.comune.roma.it/metadata 4 | KEY_LEN=3072 5 | LOCALITY_NAME=Roma 6 | MD_ALG=sha512 7 | ORGANIZATION_IDENTIFIER=PA:IT-c_h501 8 | ORGANIZATION_NAME=Comune di Roma 9 | SPID_SECTOR=public 10 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Scope of the pull request** 2 | 3 | Write here what the pull request does. Also mention any issue it solves and/or creates. 4 | 5 | **Solved issues** 6 | 7 | - #123 8 | - #456 9 | 10 | **Added issues** 11 | 12 | - #789 13 | 14 | **Tests** 15 | 16 | - [ ] I manually tested 17 | - [ ] I added unit test 18 | - [ ] I ran the existing unit test and they do not fail 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask something 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Topic** 11 | 12 | Put here the topic of your question (e.g. command line usage). 13 | 14 | **Question** 15 | 16 | Put here your question. 17 | 18 | **References** 19 | 20 | Put here references to other issues, pull requests or commits. 21 | 22 | - #123 23 | - #456 24 | - 7890abc 25 | -------------------------------------------------------------------------------- /validate-crt-with-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | CERT_FILE=${CERT_FILE:="$(pwd)/generated-certs/crt.pem"} 4 | CERT_FILE_MOUNT_POINT="/run/spid-complaint-certificates-validator/crt.pem" 5 | 6 | set -e 7 | 8 | echo "### Starting validation of certificate file at '${CERT_FILE}':\n" 9 | cat $CERT_FILE 10 | echo "" 11 | 12 | docker build --tag psmiraglia/spid-compliant-certificates-validator validator/ 13 | docker run -it --rm -v "${CERT_FILE}:${CERT_FILE_MOUNT_POINT}:ro" -e CERT_FILE="${CERT_FILE_MOUNT_POINT}" psmiraglia/spid-compliant-certificates-validator 14 | -------------------------------------------------------------------------------- /gencert-with-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ENV_FILE="`pwd`/docker.env" 4 | OUTPUT_DIR="`pwd`/generated-certs" 5 | 6 | if [ ! -f ${ENV_FILE} ]; then 7 | echo "[E] the env file '${ENV_FILE}' with the configuration for generating certificates doesn't exists" 8 | exit 1 9 | fi 10 | 11 | if [ ! -d ${OUTPUT_DIR} ]; then 12 | echo "[E] the output directory for certificates '${OUTPUT_DIR}' doesn't exists" 13 | exit 1 14 | fi 15 | 16 | docker build --tag psmiraglia/spid-compliant-certificates . 17 | docker run -it --rm --env-file "${ENV_FILE}" -v "${OUTPUT_DIR}:/output" psmiraglia/spid-compliant-certificates 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | 24 | Add any other context or screenshots about the feature request here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | **To Reproduce** 15 | 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Environment (please complete the following information):** 31 | 32 | - OS: [e.g. iOS] 33 | - Browser [e.g. chrome, safari] 34 | - Version [e.g. 22] 35 | - Other 36 | 37 | **Additional context** 38 | 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Paolo Smiraglia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | # this software and associated documentation files (the "Software"), to deal in 5 | # the Software without restriction, including without limitation the rights to 6 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | # of the Software, and to permit persons to whom the Software is furnished to do 8 | # so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2020 Paolo Smiraglia 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | # this software and associated documentation files (the "Software"), to deal in 7 | # the Software without restriction, including without limitation the rights to 8 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | # of the Software, and to permit persons to whom the Software is furnished to do 10 | # so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | set -euo pipefail 24 | 25 | # check input parameters 26 | 27 | SPID_SECTOR=${SPID_SECTOR:=""} 28 | if [ "X${SPID_SECTOR}" == "X" ]; then 29 | echo "[E] SPID_SECTOR must be set" 30 | exit 1 31 | fi 32 | 33 | case ${SPID_SECTOR} in 34 | public) 35 | gencert-public 36 | ;; 37 | private) 38 | gencert-private 39 | ;; 40 | *) 41 | echo "[E] SPID_SECTOR must be one of ['public', 'private'] but it's set to '${SPID_SECTOR}'" 42 | exit 1 43 | ;; 44 | esac 45 | -------------------------------------------------------------------------------- /validator/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Paolo Smiraglia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | # this software and associated documentation files (the "Software"), to deal in 5 | # the Software without restriction, including without limitation the rights to 6 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | # of the Software, and to permit persons to whom the Software is furnished to do 8 | # so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | FROM python:alpine3.13 22 | LABEL maintainer="Paolo Smiraglia " 23 | 24 | RUN pip install -U pip 25 | 26 | RUN apk update \ 27 | && apk add --no-cache \ 28 | gcc \ 29 | cargo \ 30 | libffi-dev \ 31 | musl-dev \ 32 | openssl-dev \ 33 | python3-dev 34 | 35 | WORKDIR /run/spid-complaint-certificates-validator/ 36 | 37 | COPY ./requirements.txt ./requirements.txt 38 | COPY ./validator.py ./validator.py 39 | 40 | RUN pip install -r requirements.txt 41 | 42 | ENTRYPOINT ["/usr/local/bin/python3", "validator.py"] 43 | CMD [] 44 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Paolo Smiraglia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | # this software and associated documentation files (the "Software"), to deal in 5 | # the Software without restriction, including without limitation the rights to 6 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | # of the Software, and to permit persons to whom the Software is furnished to do 8 | # so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | FROM alpine:latest 22 | LABEL maintainer="Paolo Smiraglia " 23 | 24 | RUN apk update \ 25 | && apk add --no-cache \ 26 | curl \ 27 | grep \ 28 | openssl \ 29 | bash 30 | 31 | COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint 32 | COPY gencert-private.sh /usr/local/bin/gencert-private 33 | COPY gencert-public.sh /usr/local/bin/gencert-public 34 | RUN chmod +x \ 35 | /usr/local/bin/docker-entrypoint \ 36 | /usr/local/bin/gencert-private \ 37 | /usr/local/bin/gencert-public 38 | ENTRYPOINT ["/usr/local/bin/docker-entrypoint"] 39 | 40 | WORKDIR /output 41 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | contatti@developers.italia.it. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 126 | at [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /gencert-private.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2021 Claudio Pizzillo 4 | # Paolo Smiraglia 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | # this software and associated documentation files (the "Software"), to deal in 8 | # the Software without restriction, including without limitation the rights to 9 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | # of the Software, and to permit persons to whom the Software is furnished to do 11 | # so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | 25 | csr="csr.pem" 26 | key="key.pem" 27 | 28 | # check input parameters 29 | 30 | KEY_LEN=${KEY_LEN:="2048"} 31 | if [ $(echo ${KEY_LEN} | grep -c -P "^(2048|3072|4096)$") -ne 1 ]; then 32 | echo "[E] KEY_LEN must be one of [2048, 3072, 4096], now ${KEY_LEN}" 33 | exit 1 34 | fi 35 | 36 | MD_ALG=${MD_ALG:="sha256"} 37 | if [ $(echo ${MD_ALG} | grep -c -P "^(sha256|sha512)$") -ne 1 ]; then 38 | echo "[E] MD_ALG must be one of [sha256, sha512], now ${MD_ALG}" 39 | exit 1 40 | fi 41 | 42 | COMMON_NAME=${COMMON_NAME:=""} 43 | if [ $(echo ${COMMON_NAME} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 44 | echo "[E] COMMON_NAME must be set" 45 | exit 1 46 | fi 47 | 48 | LOCALITY_NAME=${LOCALITY_NAME:=""} 49 | if [ $(echo ${LOCALITY_NAME} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 50 | echo "[E] LOCALITY_NAME must be set" 51 | exit 1 52 | fi 53 | 54 | ORGANIZATION_IDENTIFIER=${ORGANIZATION_IDENTIFIER:=""} 55 | if [ $(echo ${ORGANIZATION_IDENTIFIER} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 56 | echo "[E] ORGANIZATION_IDENTIFIER must be set" 57 | exit 1 58 | fi 59 | 60 | if [ $(echo ${ORGANIZATION_IDENTIFIER} | grep -c -P "^(CF:IT-[\d\w]{16}|VATIT-\d{11})$") -ne 1 ]; then 61 | echo "[E] ORGANIZATION_IDENTIFIER must be in the form of 'CF:IT-' or 'VATIT-'" 62 | exit 1 63 | fi 64 | 65 | ORGANIZATION_NAME=${ORGANIZATION_NAME:=""} 66 | if [ $(echo ${ORGANIZATION_NAME} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 67 | echo "[E] ORGANIZATION_NAME must be set" 68 | exit 1 69 | fi 70 | 71 | ENTITY_ID=${ENTITY_ID:=""} 72 | if [ $(echo ${ENTITY_ID} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 73 | echo "[E] ENTITY_ID must be set" 74 | exit 1 75 | fi 76 | 77 | # generate configuration file 78 | 79 | openssl_conf=$(mktemp) 80 | 81 | if [ $(openssl version | grep -c "OpenSSL 1.0") -ge 1 ]; then 82 | ORGID_OID="organizationIdentifier=2.5.4.97" 83 | ORGID_LABEL="2.5.4.97 organizationIdentifier organizationIdentifier" 84 | else 85 | ORGID_OID="" 86 | ORGID_LABEL="" 87 | fi 88 | 89 | cat > "${openssl_conf}" </dev/null 151 | 152 | cat < ${oids_conf} < 4 | # Paolo Smiraglia 5 | # Michele D'Amico 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | # this software and associated documentation files (the "Software"), to deal in 9 | # the Software without restriction, including without limitation the rights to 10 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 11 | # of the Software, and to permit persons to whom the Software is furnished to do 12 | # so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | 26 | csr="csr.pem" 27 | key="key.pem" 28 | 29 | # check input parameters 30 | 31 | KEY_LEN=${KEY_LEN:="2048"} 32 | if [ $(echo ${KEY_LEN} | grep -c -P "^(2048|3072|4096)$") -ne 1 ]; then 33 | echo "[E] KEY_LEN must be one of [2048, 3072, 4096], now ${KEY_LEN}" 34 | exit 1 35 | fi 36 | 37 | MD_ALG=${MD_ALG:="sha256"} 38 | if [ $(echo ${MD_ALG} | grep -c -P "^(sha256|sha512)$") -ne 1 ]; then 39 | echo "[E] MD_ALG must be one of [sha256, sha512], now ${MD_ALG}" 40 | exit 1 41 | fi 42 | 43 | COMMON_NAME=${COMMON_NAME:=""} 44 | if [ $(echo ${COMMON_NAME} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 45 | echo "[E] COMMON_NAME must be set" 46 | exit 1 47 | fi 48 | 49 | LOCALITY_NAME=${LOCALITY_NAME:=""} 50 | if [ $(echo ${LOCALITY_NAME} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 51 | echo "[E] LOCALITY_NAME must be set" 52 | exit 1 53 | fi 54 | 55 | ORGANIZATION_IDENTIFIER=${ORGANIZATION_IDENTIFIER:=""} 56 | if [ $(echo ${ORGANIZATION_IDENTIFIER} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 57 | echo "[E] ORGANIZATION_IDENTIFIER must be set" 58 | exit 1 59 | fi 60 | 61 | if [ $(echo ${ORGANIZATION_IDENTIFIER} | grep -c -P "^(CF:IT-[\d\w]{16}|VATIT-\d{11})$") -ne 1 ]; then 62 | echo "[E] ORGANIZATION_IDENTIFIER must be in the form of 'CF:IT-' or 'VATIT-'" 63 | exit 1 64 | fi 65 | 66 | ORGANIZATION_NAME=${ORGANIZATION_NAME:=""} 67 | if [ $(echo ${ORGANIZATION_NAME} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 68 | echo "[E] ORGANIZATION_NAME must be set" 69 | exit 1 70 | fi 71 | 72 | ENTITY_ID=${ENTITY_ID:=""} 73 | if [ $(echo ${ENTITY_ID} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 74 | echo "[E] ENTITY_ID must be set" 75 | exit 1 76 | fi 77 | 78 | # generate configuration file 79 | 80 | openssl_conf=$(mktemp) 81 | 82 | if [ $(openssl version | grep -c "OpenSSL 1.0") -ge 1 ]; then 83 | ORGID_OID="organizationIdentifier=2.5.4.97" 84 | ORGID_LABEL="2.5.4.97 organizationIdentifier organizationIdentifier" 85 | else 86 | ORGID_OID="" 87 | ORGID_LABEL="" 88 | fi 89 | 90 | cat > "${openssl_conf}" </dev/null 152 | 153 | cat < ${oids_conf} < myenv.sh < myenv.sh <` 165 | element 166 | (example: `https://spid.agid.gov.it`, default: `""`) 167 | 168 | * `KEY_LEN`: length of the private key 169 | (allowd values: `[2048, 3072, 4096]`, default: `2048`) 170 | 171 | * `LOCALITY_NAME`: extended name of the locality 172 | (example: `Roma`, default: `""`) 173 | 174 | * `MD_ALG`: digest algorithm to be used 175 | (allowed values: `[sha256, sha512], `default: `sha256`) 176 | 177 | * `ORGANIZATION_NAME`: extended name of the service provider 178 | (example: `Agenzia per l'Italia Digitale`, default: `""`) 179 | 180 | ### Public sector specific 181 | 182 | * `DAYS`: validity of the self-signed certificate 183 | (example: `3650`, default: `730`) 184 | 185 | * `ORGANIZATION_IDENTIFIER`: service provider identifier in the form of 186 | `PA:IT-` 187 | (example: `PA:IT-c_h501`, default: `""`) 188 | 189 | ### Private sector specific 190 | 191 | * `ORGANIZATION_IDENTIFIER`: service provider identifier in the form of 192 | `VATIT-` or `CF:IT-` 193 | (example: `VATIT-12345678901`, default: `""`) 194 | -------------------------------------------------------------------------------- /validator/validator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import base64 4 | import datetime 5 | import logging 6 | import os 7 | import unittest 8 | from typing import Tuple 9 | 10 | from cryptography import x509 11 | from cryptography.hazmat.primitives import hashes 12 | from cryptography.hazmat.primitives.asymmetric import rsa 13 | from cryptography.x509 import Extension 14 | 15 | formatter = logging.Formatter('[%(asctime)s][%(levelname)1.1s] %(message)s') 16 | fh = logging.FileHandler('validator.log', mode='w') 17 | fh.setLevel(logging.DEBUG) 18 | fh.setFormatter(formatter) 19 | LOG = logging.getLogger() 20 | LOG.setLevel(logging.DEBUG) 21 | LOG.addHandler(fh) 22 | 23 | 24 | def pem_to_der(cert_file: str) -> Tuple[bytes, str]: 25 | if not os.path.exists(cert_file): 26 | msg = 'File at %s not found' % cert_file 27 | return None, msg 28 | 29 | lines = [] 30 | with open(cert_file, 'r') as fp: 31 | lines = fp.readlines() 32 | fp.close() 33 | 34 | if ('-----BEGIN CERTIFICATE-----' not in lines[0]): 35 | msg = 'Certificate at %s must be a PEM' % cert_file 36 | return None, msg 37 | 38 | if ('-----END CERTIFICATE-----' not in lines[len(lines)-1]): 39 | msg = 'Certificate at %s must be a PEM' % cert_file 40 | return None, msg 41 | 42 | b64_data = ''.join([line[:-1] for line in lines[1:-1]]) 43 | der = base64.b64decode(b64_data) 44 | 45 | return der, None 46 | 47 | 48 | def key_usage_is_ok(e: Extension) -> bool: 49 | is_ok = False 50 | 51 | # check if extension is critical 52 | if not e.critical: 53 | return is_ok 54 | 55 | # check the requested key usage 56 | for usage in ['content_commitment', 57 | 'digital_signature']: 58 | if not getattr(e.value, usage): 59 | # requested usage is not present 60 | return is_ok 61 | 62 | # check the not requested key usage 63 | try: 64 | for usage in ['crl_sign', 65 | 'data_encipherment', 66 | 'decipher_only', 67 | 'encipher_only', 68 | 'key_agreement', 69 | 'key_cert_sign', 70 | 'key_encipherment']: 71 | if getattr(e.value, usage): 72 | # not requested usage is present 73 | return is_ok 74 | except Exception: 75 | pass 76 | 77 | # the extension is ok 78 | is_ok = True 79 | return is_ok 80 | 81 | 82 | def basic_constraints_is_ok(e: Extension) -> bool: 83 | is_ok = False 84 | 85 | # check if extension is not critical 86 | if e.critical: 87 | return is_ok 88 | 89 | # check if CA is FALSE 90 | if e.value.ca: 91 | return is_ok 92 | 93 | # the extension is ok 94 | is_ok = True 95 | return is_ok 96 | 97 | 98 | def certificate_policies_is_ok(e: Extension) -> Tuple[bool, str]: 99 | is_ok = False 100 | 101 | # check if extension is not critical 102 | if e.critical: 103 | msg = 'Extension with OID %s can\'t be critical' % e.oid.dotted_string 104 | return is_ok, msg 105 | 106 | # check policies 107 | mandatory_policies = ['1.3.76.16.6', '1.3.76.16.4.2.1'] 108 | 109 | policies = e.value 110 | cert_policies = [p.policy_identifier.dotted_string for p in policies] 111 | 112 | for p in mandatory_policies: 113 | if p not in cert_policies: 114 | msg = 'Policy %s is missing' % p 115 | return is_ok, msg 116 | 117 | for p in policies: 118 | if p.policy_identifier.dotted_string == '1.3.76.16.6': 119 | for q in p.policy_qualifiers: 120 | if isinstance(q, x509.extensions.UserNotice): 121 | if q.explicit_text != 'agIDcert': 122 | msg = ('UserNotice.ExplicitText for policy %s is not valid (%s)' # noqa 123 | % (p.policy_identifier.dotted_string, q.explicit_text)) # noqa 124 | return is_ok, msg 125 | elif p.policy_identifier.dotted_string == '1.3.76.16.4.2.1': 126 | for q in p.policy_qualifiers: 127 | if isinstance(q, x509.extensions.UserNotice): 128 | if q.explicit_text != 'cert_SP_Pub': 129 | msg = ('UserNotice.ExplicitText for policy %s is not valid (%s)' # noqa 130 | % (p.policy_identifier.dotted_string, q.explicit_text)) # noqa 131 | return is_ok, msg 132 | else: 133 | pass 134 | 135 | # the extension is ok 136 | is_ok = True 137 | return is_ok, None 138 | 139 | 140 | class TestPublicSectorSPIDCertificate(unittest.TestCase): 141 | def setUp(self): 142 | der, msg = pem_to_der(os.getenv('CERT_FILE', 'crt.pem')) 143 | if der: 144 | self.cert = x509.load_der_x509_certificate(der) 145 | else: 146 | self.fail(msg) 147 | 148 | def test_key_type_and_size(self): 149 | pk = self.cert.public_key() 150 | 151 | msg = 'The key must be RSA (now %s)' % type(pk) 152 | self.assertTrue(isinstance(pk, rsa.RSAPublicKey), msg=msg) 153 | 154 | exp_size = 2048 155 | msg = 'The key length must be >= %s (now %d)' % (exp_size, pk.key_size) 156 | self.assertGreaterEqual(pk.key_size, exp_size, msg=msg) 157 | 158 | exp_size = 4096 159 | msg = 'The key length must be <= %s (now %d)' % (exp_size, pk.key_size) 160 | self.assertLessEqual(pk.key_size, exp_size, msg) 161 | 162 | def test_digest_algorithm(self): 163 | allowed_algs = [hashes.SHA256, hashes.SHA512] 164 | alg_is_ok = False 165 | 166 | _alg = self.cert.signature_hash_algorithm 167 | for alg in allowed_algs: 168 | if isinstance(_alg, alg): 169 | alg_is_ok = True 170 | 171 | msg = 'The digest algorithm %s is not allowed' % _alg.name 172 | self.assertTrue(alg_is_ok, msg=msg) 173 | 174 | def test_mandatory_extensions(self): 175 | mandatory_exts = [ 176 | '2.5.29.15', # keyUsage 177 | '2.5.29.19', # basicConstraints 178 | '2.5.29.32', # certificatePolicies 179 | ] 180 | 181 | extensions = self.cert.extensions 182 | cert_exts = [ext.oid.dotted_string for ext in extensions] 183 | 184 | # check if all the mandatory extensions are present 185 | for ext in mandatory_exts: 186 | self.assertIn(ext, cert_exts) 187 | 188 | # check the extensions content 189 | for ext in extensions: 190 | if ext.oid.dotted_string == '2.5.29.15': 191 | self.assertTrue(key_usage_is_ok(ext)) 192 | elif ext.oid.dotted_string == '2.5.29.19': 193 | self.assertTrue(basic_constraints_is_ok(ext)) 194 | elif ext.oid.dotted_string == '2.5.29.32': 195 | res, msg = certificate_policies_is_ok(ext) 196 | self.assertTrue(res, msg=msg) 197 | else: 198 | pass 199 | 200 | def test_subject_dn(self): 201 | mandatory_attrs = [ 202 | '2.5.4.10', # organizationName 203 | '2.5.4.3', # commonName 204 | '2.5.4.6', # countryName 205 | '2.5.4.7', # localityName 206 | '2.5.4.83', # uri 207 | '2.5.4.97', # organizationIdentifier 208 | ] 209 | not_allowed_attrs = { 210 | '1.2.840.113549.1.9.1', # emailAddress 211 | '2.5.4.4', # surname 212 | '2.5.4.41', # name 213 | '2.5.4.42', # givenName 214 | '2.5.4.43', # initials 215 | '2.5.4.65', # pseudonym 216 | } 217 | 218 | subj = self.cert.subject 219 | subj_attrs = [attr.oid.dotted_string for attr in subj] 220 | 221 | # check if not allowed attrs are present 222 | for attr in not_allowed_attrs: 223 | self.assertNotIn(attr, subj_attrs) 224 | 225 | # check if all the mandatory attre are present 226 | for attr in mandatory_attrs: 227 | self.assertIn(attr, subj_attrs) 228 | 229 | # check attr the value 230 | for attr in subj: 231 | self.assertIsNotNone(attr.value) 232 | 233 | oid = attr.oid.dotted_string 234 | value = attr.value 235 | if oid == '2.5.4.97': 236 | self.assertTrue(value.startswith('PA:IT-')) 237 | elif oid == '2.5.4.6': 238 | self.assertEqual(len(value), 2) 239 | 240 | def test_expiration(self): 241 | self.assertTrue( 242 | self.cert.not_valid_after > datetime.datetime.now() 243 | ) 244 | 245 | if __name__ == '__main__': 246 | unittest.main(verbosity=int(os.getenv('VERBOSITY', '1'))) 247 | -------------------------------------------------------------------------------- /gencert-public.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2021 Paolo Smiraglia 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | # this software and associated documentation files (the "Software"), to deal in 7 | # the Software without restriction, including without limitation the rights to 8 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | # of the Software, and to permit persons to whom the Software is furnished to do 10 | # so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | # DESC: Usage help 24 | # ARGS: None 25 | # OUTS: None 26 | function script_usage() { 27 | cat << EOF 28 | create X.509 certificates according to Avviso SPID n.29 v3. 29 | 30 | Usage: 31 | -h|--help # Displays this help 32 | -i|--interactive # asks for options from console when is not set with envirement 33 | 34 | Enviroments: 35 | COMMON_NAME # string, example "Comune di Roma" 36 | DAYS # integer, example "3650" 37 | ENTITY_ID # string, example "https://spid.comune.roma.it/metadata" 38 | LOCALITY_NAME # string, example "Roma" 39 | ORGANIZATION_IDENTIFIER # string, example "PA:IT-c_h501" 40 | ORGANIZATION_NAME # string, example "Comune di Roma" 41 | MD_ALG # must be one of [sha256, sha512], default: "sha512" 42 | KEY_LEN # must be one of [2048, 3072, 4096], default: "3072" 43 | EOF 44 | } 45 | 46 | # DESC: Parameter parser 47 | # ARGS: $@ (optional): Arguments provided to the script 48 | # OUTS: Variables indicating command-line parameters and options 49 | function parse_params() { 50 | local param 51 | while [[ $# -gt 0 ]]; do 52 | param="$1" 53 | shift 54 | case $param in 55 | -h | --help) 56 | script_usage 57 | exit 0 58 | ;; 59 | -i | --interactive) 60 | INTERACTIVE=true 61 | ;; 62 | *) 63 | die "Invalid parameter was provided: $param" 1 64 | ;; 65 | esac 66 | done 67 | } 68 | 69 | # DESC: Exit script with the given message 70 | # ARGS: $1 (required): Message to print on exit 71 | # $2 (optional): Exit code (defaults to 1) 72 | # OUTS: None 73 | # NOTE: The convention used in this script for exit codes is: 74 | # 0: Normal exit 75 | # 1: Abnormal exit due to external error 76 | # 2: Abnormal exit due to script error 77 | die() { 78 | local msg=${1} 79 | local code=${2-1} # default exit status 1 80 | echo $msg 81 | exit $code 82 | } 83 | 84 | parse_params "$@" 85 | 86 | crt="crt.pem" 87 | csr="csr.pem" 88 | key="key.pem" 89 | 90 | ### check input parameters 91 | 92 | # check KEY_LEN 93 | KEY_LEN=${KEY_LEN:="3072"} 94 | if [ $(echo ${KEY_LEN} | grep -c -P "^(2048|3072|4096)$") -ne 1 ]; then 95 | die "[E] KEY_LEN must be one of [2048, 3072, 4096], now ${KEY_LEN}" 96 | fi 97 | 98 | # check MD_ALG 99 | MD_ALG=${MD_ALG:="sha512"} 100 | if [ $(echo ${MD_ALG} | grep -c -P "^(sha256|sha512)$") -ne 1 ]; then 101 | die "[E] MD_ALG must be one of [sha256, sha512], now ${MD_ALG}" 102 | fi 103 | 104 | # check COMMON_NAME 105 | if [[ -z $COMMON_NAME && -n $INTERACTIVE ]]; then 106 | read -p 'COMMON_NAME: ' COMMON_NAME 107 | fi 108 | if [ $(echo ${COMMON_NAME} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 109 | die "[E] COMMON_NAME must be set" 110 | fi 111 | 112 | # check LOCALITY_NAME 113 | if [[ -z $LOCALITY_NAME && -n $INTERACTIVE ]]; then 114 | read -p 'LOCALITY_NAME: ' LOCALITY_NAME 115 | fi 116 | if [ $(echo ${LOCALITY_NAME} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 117 | die "[E] LOCALITY_NAME must be set" 118 | fi 119 | 120 | # check ORGANIZATION_IDENTIFIER 121 | if [[ -z $ORGANIZATION_IDENTIFIER && -n $INTERACTIVE ]]; then 122 | read -p 'ORGANIZATION_IDENTIFIER: ' ORGANIZATION_IDENTIFIER 123 | fi 124 | if [ $(echo ${ORGANIZATION_IDENTIFIER} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 125 | die "[E] ORGANIZATION_IDENTIFIER must be set" 126 | fi 127 | if [ $(echo ${ORGANIZATION_IDENTIFIER} | grep -c '^PA:IT-') -ne 1 ]; then 128 | die "[E] ORGANIZATION_IDENTIFIER must be in the format of 'PA:IT-'" 129 | fi 130 | IPA_CODE=$(echo ${ORGANIZATION_IDENTIFIER} | sed -e "s/PA:IT-//g") 131 | JSON1='{"paginazione":{"campoOrdinamento":"codAoo","tipoOrdinamento":"asc","paginaRichiesta":1,"numTotalePagine":null,"numeroRigheTotali":null,"paginaCorrente":null,"righePerPagina":null},"codiceFiscale":null,"codUniAoo":null,"desAoo":null,"denominazioneEnte":null,"codEnte":"' 132 | JSON2='","codiceCategoria":null,"area":null}' 133 | JSON="${JSON1}${IPA_CODE}${JSON2}" 134 | if curl -X POST https://indicepa.gov.it/PortaleServices/api/aoo -H "Content-Type: application/json" -d ${JSON} | grep -qv '"numeroRigheTotali":1'; then 135 | die "[E] ORGANIZATION_IDENTIFIER refers to something that does not exists \n [I] Check it by yourself at ${CHECK_URL}" 136 | fi 137 | 138 | # check ORGANIZATION_NAME 139 | if [[ -z $ORGANIZATION_NAME && -n $INTERACTIVE ]]; then 140 | read -p 'ORGANIZATION_NAME: ' ORGANIZATION_NAME 141 | fi 142 | if [ $(echo ${ORGANIZATION_NAME} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 143 | die "[E] ORGANIZATION_NAME must be set" 144 | fi 145 | 146 | # check ENTITY_ID 147 | if [[ -z $ENTITY_ID && -n $INTERACTIVE ]]; then 148 | read -p 'ENTITY_ID: ' ENTITY_ID 149 | fi 150 | if [ $(echo ${ENTITY_ID} | grep -Pc "^\S(.*\S)?$") -ne 1 ]; then 151 | die "[E] ENTITY_ID must be set" 152 | fi 153 | 154 | # generate configuration file 155 | 156 | openssl_conf=$(mktemp) 157 | 158 | if [ $(openssl version | grep -c "OpenSSL 1.0") -ge 1 ]; then 159 | ORGID_OID="organizationIdentifier=2.5.4.97" 160 | ORGID_LABEL="2.5.4.97 organizationIdentifier organizationIdentifier" 161 | else 162 | ORGID_OID="" 163 | ORGID_LABEL="" 164 | fi 165 | 166 | cat > ${openssl_conf} </dev/null 226 | 227 | cat </dev/null 242 | 243 | cat < ${oids_conf} <