├── .gitignore ├── HOWTO.md ├── LICENSE ├── Makefile ├── README.md ├── config_template.json ├── packaging ├── .gitignore ├── README.md ├── deb │ ├── Dockerfile │ ├── build.sh │ └── debian │ │ ├── changelog │ │ ├── compat │ │ ├── control │ │ ├── copyright │ │ ├── rules │ │ └── source │ │ └── format └── rpm │ ├── Dockerfile │ └── pamoauth2device.spec ├── src ├── include │ ├── config.cpp │ ├── config.hpp │ ├── ldapquery.c │ ├── ldapquery.h │ ├── metadata.cpp │ ├── metadata.hpp │ ├── nayuki │ │ ├── BitBuffer.cpp │ │ ├── BitBuffer.hpp │ │ ├── QrCode.cpp │ │ ├── QrCode.hpp │ │ ├── QrSegment.cpp │ │ └── QrSegment.hpp │ ├── nlohmann │ │ └── json.hpp │ ├── pam_oauth2_curl.cpp │ ├── pam_oauth2_curl.hpp │ ├── pam_oauth2_curl_impl.hpp │ ├── pam_oauth2_excpt.hpp │ ├── pam_oauth2_log.cpp │ └── pam_oauth2_log.hpp ├── pam_oauth2_device.cpp └── pam_oauth2_device.hpp ├── test ├── Makefile ├── README.md ├── data │ ├── qr1.0.txt │ ├── qr1.1.txt │ ├── qr1.2.txt │ ├── qr2.0.txt │ ├── qr2.1.txt │ ├── qr2.2.txt │ ├── template_empty.json │ ├── template_noldap.json │ └── template_wrong.json ├── mock_server.py ├── temp_file.cpp ├── temp_file.hpp ├── test_config.cpp ├── test_pam_oauth2_device.cpp └── unit.cpp └── util └── tls-debug ├── README.md └── tls-debug.c /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | *.o 11 | *.a 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Environments 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | env.bak/ 95 | venv.bak/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | ### Windows template 110 | # Windows thumbnail cache files 111 | Thumbs.db 112 | ehthumbs.db 113 | ehthumbs_vista.db 114 | 115 | # Dump file 116 | *.stackdump 117 | 118 | # Folder config file 119 | [Dd]esktop.ini 120 | 121 | # Recycle Bin used on file shares 122 | $RECYCLE.BIN/ 123 | 124 | # Windows Installer files 125 | *.cab 126 | *.msi 127 | *.msix 128 | *.msm 129 | *.msp 130 | 131 | # Windows shortcuts 132 | *.lnk 133 | ### macOS template 134 | # General 135 | .DS_Store 136 | .AppleDouble 137 | .LSOverride 138 | 139 | # Icon must end with two \r 140 | Icon 141 | 142 | # Thumbnails 143 | ._* 144 | 145 | # Files that might appear in the root of a volume 146 | .DocumentRevisions-V100 147 | .fseventsd 148 | .Spotlight-V100 149 | .TemporaryItems 150 | .Trashes 151 | .VolumeIcon.icns 152 | .com.apple.timemachine.donotpresent 153 | 154 | # Directories potentially created on remote AFP share 155 | .AppleDB 156 | .AppleDesktop 157 | Network Trash Folder 158 | Temporary Items 159 | .apdisk 160 | ### JetBrains template 161 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 162 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 163 | 164 | # User-specific stuff 165 | .idea/**/workspace.xml 166 | .idea/**/tasks.xml 167 | .idea/**/dictionaries 168 | .idea/**/shelf 169 | 170 | # Sensitive or high-churn files 171 | .idea/**/dataSources/ 172 | .idea/**/dataSources.ids 173 | .idea/**/dataSources.local.xml 174 | .idea/**/sqlDataSources.xml 175 | .idea/**/dynamic.xml 176 | .idea/**/uiDesigner.xml 177 | .idea/**/dbnavigator.xml 178 | 179 | # Gradle 180 | .idea/**/gradle.xml 181 | .idea/**/libraries 182 | 183 | # CMake 184 | cmake-build-debug/ 185 | cmake-build-release/ 186 | 187 | # Mongo Explorer plugin 188 | .idea/**/mongoSettings.xml 189 | 190 | # File-based project format 191 | *.iws 192 | 193 | # IntelliJ 194 | out/ 195 | 196 | # mpeltonen/sbt-idea plugin 197 | .idea_modules/ 198 | 199 | # JIRA plugin 200 | atlassian-ide-plugin.xml 201 | 202 | # Cursive Clojure plugin 203 | .idea/replstate.xml 204 | 205 | # Crashlytics plugin (for Android Studio and IntelliJ) 206 | com_crashlytics_export_strings.xml 207 | crashlytics.properties 208 | crashlytics-build.properties 209 | fabric.properties 210 | 211 | # Editor-based Rest Client 212 | .idea/httpRequests 213 | ### Linux template 214 | *~ 215 | 216 | # temporary files which can be created if a process still has a handle open of a deleted file 217 | .fuse_hidden* 218 | 219 | # KDE directory preferences 220 | .directory 221 | 222 | # Linux trash folder which might appear on any partition or disk 223 | .Trash-* 224 | 225 | # .nfs files are created when an open file is removed but is still being accessed 226 | .nfs* 227 | 228 | # Google C++ test library 229 | googletest* 230 | -------------------------------------------------------------------------------- /HOWTO.md: -------------------------------------------------------------------------------- 1 | # OIDC Login PAM Module HOWTO 2 | 3 | This is a HOWTO document describing how to use the OIDC device flow authenticator PAM module as refactored and augmented 4 | by the [IRIS](www.iris.ac.uk) team. This work was funded by [STFC](https://www.ukri.org/councils/stfc/), a part of 5 | [UK Research and Innovation (UKRI)](https://www.ukri.org/). 6 | 7 | ## A Tale of Two (or Three) User Ids 8 | 9 | Users who authenticate with this module have two or three user ids. 10 | 11 | * A *remote* user id, picked from the userinfo record published by the IAM. 12 | * A *local* user id, requested by the user on the ssh command line (or similar login tool) 13 | * A possibly-suffixed local id, which is used to put a common suffix on all local accounts that users are allowed to 14 | log into using this module. See below. 15 | 16 | ## How to configure sshd to use the module 17 | 18 | Your PAM configuration needs to include this module. A typical example is: 19 | 20 | ``` 21 | #%PAM-1.0 22 | auth required pam_sepermit.so 23 | auth required pam_oauth2_device.so 24 | account required pam_nologin.so 25 | account required pam_oauth2_device.so 26 | ``` 27 | (and it would then continue with session entries). At the time of writing, the entry for this module in the account 28 | section does nothing (i.e., it always returns success), but this may change in future releases. 29 | 30 | This file typically needs to be stored in the directory `/etc/pam.d` with the name of the executable of the ssh daemon 31 | (e.g. `/etc/pam.d/sshd`). 32 | 33 | ## How to combine this module with other authentication methods 34 | 35 | The *bypass* feature allows some users to completely skip the authentication (and authorisation) steps of the module. 36 | Bypass is available in two flavours, one using an LDAP callout and the other using a local username lookup. 37 | 38 | In order to use the bypass feature, there must be other authentication/authorisation modules in the PAM configuration. 39 | If not, login will fail. 40 | 41 | ### Bypass configuration 42 | 43 | Currently there are two methods of bypassing the module. 44 | 45 | #### Local Users 46 | 47 | The `users` section of the configuration file can have a special entry called `*bypass*` where normally the remote 48 | username would be specified. So the section would be 49 | 50 | ``` 51 | "users": { 52 | "*bypass*": 53 | [ 54 | "root", 55 | "fred" 56 | ], 57 | "remote1@example.com": 58 | [ 59 | "znap" 60 | ] 61 | } 62 | ``` 63 | 64 | This section of the confiuration would ask the PAM module to skip the OIDC authentication/authorisation for login as 65 | (local) user root and user fred. There is no remote username in this case, because the OIDC authentication is skipped, 66 | and the username suffix (if defined) is thus ignored for this check. 67 | 68 | #### LDAP 69 | 70 | If LDAP is configured, the attribute `preauth` can be defined (it is not clear why it is called "preauth" and not 71 | "bypass" but that is what it does). It should contain an LDAP query in which the first occurrences of `%s` gets the 72 | local username substituted into the query. The OIDC login is bypassed if and only if the LDAP query is successful 73 | (regardless of what it returns). 74 | 75 | ``` 76 | "ldap": { 77 | "host": "ldaps://ldap.example.com:636", 78 | "basedn": "ou=users,dc=example,dc=com", 79 | "user": "system", 80 | "passwd": "abc123@321cba", 81 | "filter": "(&(objectclass=user)(uid=%s))", 82 | "preauth": "(&(objectclass=user)(cn=%s)(department=scd))", 83 | "attr": "cn" 84 | } 85 | ``` 86 | 87 | This is for a system where a username and password are required to query the LDAP server. We assume that user records: 88 | 89 | * Are identified with `(objectclass=user)` 90 | * Have a `department` entry 91 | * Have a `uid` entry which should match the **remote** user id (i.e. the id that IAM expects you to have) 92 | * Have a `cn` (commonName) entry which matches the **local** user id 93 | 94 | Here, users have records that are searched with the (default subtree) search; the filter would select user records 95 | (assuming the cn (commonName) is the string users would use as their local id). Users who additionally have a 96 | department attribute with a value of "scd" would be bypassed. 97 | 98 | For users who are *not* bypassed, the value of the cn attribute in the user object is looked up and compared to the id 99 | requested by the user. 100 | 101 | Once again, 102 | 103 | * The filter uses the **remote** id to look up a record 104 | * The preauth (bypass) feature uses the **local** id because there is no remote id prior to the module running 105 | 106 | ### Configuration other authentication methods 107 | 108 | The bypass feature was designed to selectively let the module say "don't know" about a set of users, rather than "yes" 109 | (authentication successful) or "no" (authentication unsuccessful). 110 | 111 | However, this presumes there is a fallback option (if there isn't, authentication will fail.) There are two 112 | possibilities: have sshd use another method, or configure another method in PAM. 113 | 114 | #### Configuring other authentication methods in PAM 115 | 116 | 117 | 118 | 119 | #### Configuring other authentication methods in sshd 120 | 121 | Assuming the ssh daemon -- and the client -- come from the OpenSSH implementation, the client tries the following 122 | methods: 123 | 124 | - Host based authentication 125 | - Public key authentication 126 | - Challenge/response authentication 127 | - This includes the PAM authentication 128 | - Password authentication 129 | 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 ICS-MU 190 | Copyright 2020 UKRI STFC 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CXXFLAGS=-Wall -fPIC -std=c++11 2 | CFLAGS=-Wall -fPIC 3 | 4 | LDLIBS=-lpam -lcurl -lldap -llber 5 | 6 | objects = src/pam_oauth2_device.o \ 7 | src/include/config.o \ 8 | src/include/metadata.o \ 9 | src/include/pam_oauth2_curl.o \ 10 | src/include/pam_oauth2_log.o \ 11 | src/include/ldapquery.o \ 12 | src/include/nayuki/BitBuffer.o \ 13 | src/include/nayuki/QrCode.o \ 14 | src/include/nayuki/QrSegment.o 15 | 16 | all: pam_oauth2_device.so 17 | 18 | %.o: %.c %.h 19 | $(CC) $(CFLAGS) -c $< -o $@ 20 | 21 | %.o: %.cpp %.hpp 22 | $(CXX) $(CXXFLAGS) -c $< -o $@ 23 | 24 | pam_oauth2_device.so: $(objects) 25 | $(CXX) -shared $^ $(LDLIBS) -o $@ 26 | 27 | .PHONY: clean distclean install 28 | clean: 29 | rm -f $(objects) 30 | 31 | distclean: clean 32 | rm -f pam_oauth2_device.so 33 | 34 | install: pam_oauth2_device.so 35 | install -D -t $(DESTDIR)$(PREFIX)/lib/security pam_oauth2_device.so 36 | install -m 600 -D config_template.json $(DESTDIR)$(PREFIX)/etc/pam_oauth2_device/config.json 37 | 38 | .PHONY: test tests 39 | 40 | test tests: 41 | make -C test 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PAM module for OAuth 2.0 Device flow 2 | 3 | This is a PAM module that lets you log in via SSH to servers using OpenID Connect credentials, instead of SSH Keys or a username and password combination. 4 | 5 | It uses the OAuth2 Device Flow, which means that during the login process, you will click a link and log in to your OpenID Connect Provider, which will then authenticate you for the SSH session. 6 | 7 | This module will then check if you're in the right group(s) or have a specified username, and allow or deny access. 8 | 9 | A demo video is avaliable here: https://drive.google.com/file/d/1WzDRL0RFDXfvUgabbXNzBppV-DKXyUN1/view?usp=sharing 10 | 11 | This code was originally developed by [Mazarykova Univerzita](https://github.com/ICS-MU/pam_oauth2_device) and [this branch](https://github.com/stfc/pam_oauth2_device) has been refactored by UKRI-STFC as a part of the [IRIS](https://www.iris.ac.uk/) activity. This work was funded by [STFC](https://www.ukri.org/councils/stfc/), a part of [UK Research and Innovation (UKRI)](https://www.ukri.org/). 12 | 13 | 14 | ## Build 15 | 16 | The upstream build uses basic `make` and we have stuck with this for compatibility reasons. 17 | The two basic targets are `make` and `make test`; the latter will build (some of) the tests and run them. 18 | 19 | * Note that at present some of the integration tests are failing. 20 | * Note you can build RPMs and DEBs, though currently they are designed for CentOS7 and Ubuntu 18.04 respectively. 21 | * The build currently requires Docker (or compatible) 22 | 23 | 24 | ### Manual Build on Scientific Linux or CentOS7 25 | 26 | ``` 27 | yum install epel-release 28 | yum install openldap-devel 29 | yum install libcurl-devel 30 | yum install pam-devel 31 | yum install libldb-devel 32 | yum install http://ftp.scientificlinux.org/linux/scientific/7x/external_products/softwarecollections/yum-conf-softwarecollections-2.0-1.el7.noarch.rpm 33 | yum install devtoolset-8 # this is needed as we need a more up to date g++ version than is supplied by default in SL repos. 34 | scl enable devtoolset-8 bash 35 | git clone https://github.com/stfc/pam_oauth2_device.git 36 | cd pam_oauth2_device/ 37 | make 38 | cp pam_oauth2_device.so /lib64/security/pam_oauth2_device.so 39 | cp config_template.json config.json 40 | ``` 41 | 42 | ### Build on Debian 10/Ubuntu Focal (20.04) 43 | 44 | 45 | ## Installation 46 | 47 | To install the module, copy `pam_oauth2_device.so` into the PAM modules directory (usually with permissions 0755). 48 | 49 | On Debian-based systems, this would be `/lib/x86_64-linux-gnu/security` whereas CentOS and related flavours would use `/usr/lib64/security`. If in doubt, check `dpkg --L libpam-modules` or `rpm -ql pam` respectively. 50 | 51 | 52 | ## Configuration 53 | 54 | Although JSON is not ideal for configuration files (it's a bit picky about syntax and it's hard to add comments), we now 55 | support splitting the configuration file into several segments (all of which should be JSON "objects" at the outermost 56 | level). This is useful for maintaining parts of the file such as client secrets or user maps in separate files. 57 | 58 | ### Splitting configuration into several files 59 | 60 | In any place where a JSON object (the curly braces construct) is expected in the configuration, a JSON string can be 61 | placed instead with a filename in the string; this file will be loaded and will serve as the object in question and must 62 | have the format expected in the configuration template. This can be nested to any depth required. It is strongly 63 | recommended to use full pathnames inside these strings. 64 | 65 | For example, if the normal configuration file looks like this (obviously a real configuration file would have more stuff 66 | in it): 67 | 68 | ``` 69 | { 70 | "oauth": { 71 | "client": { 72 | "id": "abcd", 73 | "secret": "meetmeinstlouis" 74 | }, 75 | "scope": "openid profile" 76 | } 77 | ``` 78 | and the system administrator decides to store the client object in a separate file, they can do it as follows: 79 | 80 | ``` 81 | { 82 | "oauth": { 83 | "client": "/etc/pam_oauth2_device/client.json", 84 | "scope": "openid profile" 85 | } 86 | ``` 87 | The `client.json` file should then look like this: 88 | ``` 89 | { 90 | "id": "abcd", 91 | "secret": "meetmeinstlouis" 92 | } 93 | ``` 94 | 95 | In contrast, the `scope` cannot be split off into a separate file as a string is expected as value for the `scope` parameter. 96 | 97 | ### User names 98 | 99 | Usernames are mentioned several times in this document and could probably get a bit confusing. This section attempts to give a short explanation. 100 | 101 | For every user there are *three* usernames, which can be distinct. 102 | 103 | The **local user name** is the name the user uses in the ssh login, as in `ssh fred@example.com` where the local user name is `fred`. This is the name that is passed into the PAM module for authentication. 104 | 105 | The **remote user name** is the corresponding name for the user as held by the IAM system. Once the user has successfully authenticated to IAM, IAM publishes a "userinfo" structure with the user's name and email address and other attributes that IAM can assert. Within this structure, the PAM module can pick an attribute to use as the remote user name (using the `username_attribute` option). 106 | 107 | The **account name** is the name of the local Unix account that the user is mapped into once they have authenticated. By default, it is the same as the local user name. 108 | 109 | ### Summary of Configuration Options 110 | 111 | The template `config_template.json` should give an outline of the configuration file. 112 | 113 | The configuration should be installed at `/etc/pam_oauth2_device/config.json` (a future release should make it configurable, to allow different modules to coexist). 114 | 115 | As the name suggests, the file is in JSON, so it is recommended to check it with a JSON validator like `jq` after editing it (the PAM module will refuse to load an invalid JSON file, but you will not see this error till runtime.) 116 | 117 | Thus, at the top level, there is a single object with a number of entries, described as "sections" in the table: 118 | 119 | #### Table 1: Configuring Authentication Flow 120 | 121 | | Section | Entry | Type | Req'd | Description | Notes | 122 | | --- | --- | --- | --- | --- | --- | 123 | | oauth | | Object | Y | | | 124 | | oauth | client | Object | Y | Contains "id" and "secret" | | 125 | | oauth | scope | String | Y | OIDC scope | Note 1 | 126 | | oauth | device\_endpoint | String | Y | Device endpoint | https://${url}/devicecode | 127 | | oauth | token\_endpoint | String | Y | Token endpoint | https://${url}/token | 128 | | oauth | userinfo\_endpoint | String | Y | Userinfo | https://${url}/userinfo | 129 | | oauth | username\_attribute | String | Y | Attribute for remote username | | 130 | | oauth | local\_username\_suffix | String | Y | See usernames | | 131 | | tls | | Object | Y | | | 132 | | tls | ca\_bundle | String | N | Concatenated list of trust anchors | Note 2 | 133 | | tls | ca\_path | String | N | Directory with trust anchors | Note 2 | 134 | | qr | | Object | N | | | 135 | | qr | error\_correction\_level | Int | Y | QR code | Note 3 | 136 | 137 | Notes: 138 | 139 | 1 The string value should have a _space separated_ list of scopes which must include `offline_access` 140 | 2 If present, the "ca\_bundle" must be a file with PEM-formatted trust anchors (CA certificates) concatenated together. 141 | * "ca\_path" works only with OpenSSL 142 | * On the target system, use `curl -V` to see whether curl uses OpenSSL or NSS 143 | * If both ca\_path and ca\_bundle are present, the latter takes precedence 144 | 3 The QR code section is optional but if present, it must have the error correction level defined. Permitted values are 1 (low), 2 (medium), 3 (high) or -1 (disabled). If the section is missing, the QR code is disabled. 145 | 4 The "${url}" above would be the URL (hostname) of your OpenID Provider. Its host certificate must be valid when checked against the CA bundle (see item 2) 146 | 147 | #### Table 2: Configuring Authorisation Flow 148 | 149 | No authorisation section is required, although if included there will be necessary entries in the section. However, if no authorisation section is provided, the user will not be able to log in. 150 | 151 | This part of the module functionality carries a lot of legacy stuff; see the Authorisation section for discussion. 152 | 153 | | Section | Entry | Type | Req'd | Description | Notes | 154 | | --- | --- | --- | --- | --- | --- | 155 | | ldap | | Object | N | LDAP configuration | | 156 | | ldap | host | String | Y | LDAP URL | | 157 | | ldap | basedn | String | Y | Base DN | Note 1 | 158 | | ldap | user | String | Y | username | Note 2 | 159 | | ldap | passwd | String | Y | password | Note 2 | 160 | | ldap | scope | String | N | | Note 3 | 161 | | ldap | preauth | String | N | | | 162 | | ldap | filter | String | Y | | | 163 | | ldap | attr | String | Y | | | 164 | | group | | Object | N | | | 165 | | group | access | boolean | Y | Whether to use this section | | 166 | | group | service\_name | String | Y | | Note 4 | 167 | | cloud | | Object | N | | | 168 | | cloud | access | boolean | Y | Whether to use this section | | 169 | | cloud | endpoint | String | Y | | Note 5 | 170 | | cloud | username | String | Y | endpoint username | Note 5 | 171 | | cloud | metadata\_file | String | Y | | Note 5 | 172 | | users | | Object | N | User Mapping section | Note 6 | 173 | 174 | 1 The base DN, least significant RDN first 175 | 2 Username and password are for authentication to the LDAP server, if used; if not used, just leave them as empty strings 176 | 3 scope is one of 'sub'/'subtree', 'one'/'onelevel' or 'base'/'baseobject' 177 | - If the LDAP implementation supports 'subordinate' or 'children' (these are synonymous) then these are available as scopes as well 178 | - The default is 'sub' 179 | 4 The service name is a string, which should name a group 180 | 5 The 'cloud' section implements a callout to a server to fetch a group membership file 181 | 6 The 'users' section provides mappings from the username attribute (selected with username\_attribute) to a local user id. 182 | 183 | ### Bypass 184 | 185 | The module provides a feature for bypassing authentication altogether, letting the process fall through to the next PAM 186 | module in the stack (provided the PAM authentication is configured correctly; see the HOWTO for further details.) A 187 | typical use case is to treat root logins separately. 188 | 189 | There are currently two ways of bypassing; one is an LDAP lookup based on the "preauth" query, and the other is a 190 | special 'users' section where the remote username is the magic string `*bypass*`; if the local username is in this 191 | section, the PAM module is bypassed. See the HOWTO for further details. 192 | 193 | ### Deprecated? 194 | 195 | - Future releases should change the `client_debug` to loglevel. 196 | - Additionally, adding `debug` to the PAM config should enable debug, as expected for a PAM module 197 | - The metadata file called `project_id` currently has a backwards compatible default of `/mnt/context/openstack/latest/meta_data.json` 198 | 199 | 200 | ## Authorisation 201 | 202 | The major refactoring of the module in Sep 2021 preserved (and bugfixed) the existing authorisation functionality. However, the user should be warned that it is subject to revision, but generally preserving backward functionality if possible. 203 | 204 | ### Comprehensive Example 205 | 206 | As above, user Fred Bloggs logs in with `ssh fred@example.com`. The host at `example.com` asks Fred to authenticate. Once successfully, it calls out to IAM to obtain the userinfo structure. From this it picks the attribute specified with `username_attribute` in the configuration, `preferred_username`, say. Let's say the value of `preferred_username` of Fred's userinfo structure is `bloggs`. Additionally, the userinfo structure contains the list of groups "users", "iris" and "cloud". 207 | 208 | Throughout the rest of this section, it is assumed that Fred has authenticated successfully to IAM. 209 | 210 | If the **cloud** section is configured and `access` is true, a local file configured as the `metadata_file` is read. This file should contain the structure 211 | 212 | ``` 213 | {"project\_id": "fleeps"} 214 | ``` 215 | 216 | The module adds the string `fleeps` to the endpoint (with a slash) and calls the server (with *no* client authentication) to see what is at the endpoint. It expects a JSON structure as response, structured like 217 | 218 | ``` 219 | {"groups": ["wop", "fap", "foo", "users"]} 220 | ``` 221 | 222 | If one of these groups matches Fred's groups as returned by the userinfo structure (it does here, "users"), then Fred is considered authorised. An additional check is made whether the local username plus suffix equals the remote username. If this check and the cloud group membership both pass, then Fred is considered authorised (the username check would fail in this example, because no suffix can make 'fred' equal to 'bloggs'.) 223 | 224 | 225 | If the **group** section is configured and `access` is set to true, a check will be made whether the configured value for `service_name` is one of Fred's groups. Note the service name is single valued. Additionally, as for the cloud section, the local username plus suffix must equal the remote username. 226 | 227 | If Fred's remote username were `fred_fleeps` then it *would* match the local username (`fred`) if the suffix were configured as `_fleeps`. 228 | 229 | If Fred is not authorised through the cloud or group sections, either because the check failed or they were not enabled, then a configured usermap is consulted. This is written straight into the configuration file (it should probably be in its own file at some point), so would be suitable only for a smallish number of users. 230 | 231 | This usermap is in the **users* section which expects a JSON object mapping the *remote* username to an array of permissible local usernames. Thus, the same user could have multiple local logins using this method. If the local username is found here, Fred is considered authorised. No suffix is used in this section. 232 | 233 | If Fred is not authorised through any of these methods, the module falls back to an LDAP lookup (if the **ldap** section is configured). The LDAP query takes a configured filter and substitutes the *remote* username for a `%s` part of the filter, and queries a configured attribute. 234 | 235 | If the filter is `(&(objectClass=user)(cn=%s))` then `bloggs` is substituted in our example, and a target attribute (configured with `attr`) is queried from the LDAP server. For example, if `attr` has the value `uid` then the equivalent of 236 | 237 | ``` 238 | ldapsearch -x -H ldap://host -b base '(&(objectClass=user)(cn=bloggs))' uid 239 | ``` 240 | 241 | is run and the result is compared with the local username, `fred`. If these match (no suffix is used), then Fred is again considered authorised. 242 | 243 | 244 | ## SSH Configuration 245 | 246 | You MUST edit the configuration before this module will work! 247 | 248 | Make sure the module works correctly before changing your SSH config or you may be locked out! See Testing below. 249 | 250 | Edit `/etc/pam.d/sshd` and comment out the other `auth` sections (unless you need MFA or something else). 251 | 252 | ``` 253 | auth required pam_oauth2_device.so 254 | ``` 255 | 256 | Edit `/etc/ssh/sshd_config` and make sure that the following configuration options are set 257 | 258 | ``` 259 | ChallengeResponseAuthentication yes 260 | UsePAM yes 261 | ``` 262 | 263 | ``` 264 | systemctl restart sshd 265 | ``` 266 | 267 | ## Testing the module works 268 | 269 | You are advised to do this before making changes to your main SSH config. There are two tests to do which are recommended to do in the order described here. 270 | 271 | ### Preparing for the tests 272 | 273 | It is recommended that you create a (hard or sym) link to your `sshd` called (for example) `pamsshd`, e.g. `/usr/local/sbin/pamsshd`. This means you can have a PAM configuration for `pamsshd` which is different from the normal `sshd`. 274 | 275 | In this case you can copy `/etc/pam.d/sshd` to `/etc/pam.d/pamsshd` and edit the latter, leaving the former to log you back into the system if something goes wrong. Also copy `/etc/ssh/sshd_config` to `/etc/ssh/pamsshd_config` so you can edit the configuration for `pamsshd` separately. 276 | 277 | Note that testing *requires* that you install the module in the system location and you have the configuration set up in `/etc/pam_oauth2_device/config.json` and `/etc/pam.d/` and `/etc/ssh/`. 278 | 279 | ### Test 1: pam tester 280 | 281 | Follow instructions above, and additionally install `pamtester`. 282 | 283 | Run 284 | ``` 285 | pamtester -v pamtester localusername authenticate 286 | ``` 287 | and follow the onscreen prompts. Here, `localusername` refers to your local user name so replace it with whatever your name is. 288 | 289 | You can check `/var/log/secure` or `/var/log/auth.log` to find what's wrong if there are errors authenticating. While the module 290 | uses syslog, syslog is normally set up to log PAM stuff into one of these files. 291 | 292 | ### Test 2: sshd 293 | 294 | While pamtester tests the authenticate section, you should try a proper ssh login from another host. If you created `pamsshd` as above (and copied configuration as described above), start it with 295 | 296 | ``` 297 | /usr/local/sbin/pamsshd -f /etc/ssh/pamsshd_config -p 2222 -d 298 | ``` 299 | 300 | This should start `sshd` with the name `pamsshd` listening on port 2222. Now try to log in from another host (bearing in mind the port should be open for incoming tcp). On the other host, run `ssh -p 2222 localusername@myhost` where `localusername` is the local user name and `myhost` is the host running `pamsshd`. 301 | 302 | Again check the logs as in the previous tests. 303 | 304 | ## Notable Versions 305 | 306 | - v1.03 support for segmented configuration files (as described above) 307 | - v1.00 (commit efd50f6376a65319a2834b957a2cf6bbe6b7aa2d) - first version for full scale DiRAC test 308 | -------------------------------------------------------------------------------- /config_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "oauth": { 3 | "client": { 4 | "id": "client_id", 5 | "secret": "client_secret" 6 | }, 7 | "scope": "openid profile", 8 | "device_endpoint":"https://provider.com/devicecode", 9 | "token_endpoint": "https://provider.com/token", 10 | "userinfo_endpoint": "https://provider.com/userinfo", 11 | "username_attribute": "preferred_username", 12 | "local_username_suffix": "" 13 | }, 14 | "tls": { 15 | "ca_path": "/etc/pki/certificates" 16 | }, 17 | "ldap": { 18 | "host": "ldaps://ldap-server:636", 19 | "basedn": "basedn", 20 | "user": "user", 21 | "passwd": "password", 22 | "filter": "(&(objectClass=user)(fedid=%s))", 23 | "attr": "uid" 24 | }, 25 | "qr": { 26 | "error_correction_level": 0 27 | }, 28 | "group": { 29 | "access": true, 30 | "service_name": "stfc-cloud-prod" 31 | }, 32 | "cloud": { 33 | "endpoint": "http://host-172-16-114-198.nubes.stfc.ac.uk", 34 | "username": "root", 35 | "access": false, 36 | "metadata_file": "metadata.json" 37 | }, 38 | "users": { 39 | "*bypass*": 40 | [ 41 | "root" 42 | ], 43 | "provider_user_id_1": 44 | [ 45 | "bob" 46 | ], 47 | "provider_user_id_2": 48 | [ 49 | "mike" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packaging/.gitignore: -------------------------------------------------------------------------------- 1 | !*.spec 2 | deb/pamoauth2device* 3 | rpm/RPMS -------------------------------------------------------------------------------- /packaging/README.md: -------------------------------------------------------------------------------- 1 | # Packaging pam_oauth2_device 2 | 3 | ## Building a deb package 4 | 5 | (Tested on Ubuntu 18.04) 6 | 7 | 1. Update package metadata in the `debian` directory. Specifically, update the 8 | `changelog` file. Update pam_oauth2_device version in `deb/build.sh` and 9 | `deb/Dockerfile`. 10 | 2. Follow the commands in `deb/build.sh` script to build the package. 11 | Alternatively, build the package in a docker container `deb/build.sh` 12 | (signing is currently not supported). 13 | 14 | ```bash 15 | docker build -t pamoauth2device-deb-build . 16 | docker run --rm -v ${PWD}:/data pamoauth2device-deb-build bash -c 'cp *.deb /data' 17 | ``` 18 | 19 | ## Building a rpm package 20 | 21 | 1. Update pam_oauth2_device version in `rpm/pamoauth2device.spec` and 22 | `rpm/Dockerfile` files. Update change log in `rpm/pamoauth2device.spec`. 23 | 2. In the `rpm` directory, build the container and extract the rpm file. 24 | 25 | ```bash 26 | docker build -t pamoauth2device-rpm-build . 27 | docker run --rm -v ${PWD}:/data pamoauth2device-rpm-build cp -r 'rpmbuild/RPMS /data' 28 | ``` 29 | -------------------------------------------------------------------------------- /packaging/deb/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | build-essential \ 5 | debhelper \ 6 | devscripts \ 7 | libcurl4-openssl-dev \ 8 | libldap2-dev \ 9 | libpam0g-dev \ 10 | && rm -rf /var/lib/apt/lists/* 11 | 12 | RUN groupadd builder \ 13 | && useradd --create-home --gid builder builder 14 | 15 | USER builder 16 | 17 | ENV PACKAGE_NAME=pamoauth2device \ 18 | PACKAGE_VERSION=0.1.1 \ 19 | PACKAGE_URL=https://github.com/jsurkont/pam_oauth2_device 20 | 21 | WORKDIR /home/builder 22 | 23 | COPY --chown=builder:builder debian debian 24 | 25 | RUN curl -L ${PACKAGE_URL}/archive/v${PACKAGE_VERSION}.tar.gz -o ${PACKAGE_NAME}_${PACKAGE_VERSION}.orig.tar.gz \ 26 | && mkdir build \ 27 | && tar -xzf ${PACKAGE_NAME}_${PACKAGE_VERSION}.orig.tar.gz -C build --strip-components 1 \ 28 | && cp -r debian build \ 29 | && cd build \ 30 | && debuild 31 | -------------------------------------------------------------------------------- /packaging/deb/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NAME=pamoauth2device 4 | VERSION=0.1.1 5 | URL_REPO=https://github.com/jsurkont/pam_oauth2_device 6 | BUILD_DIR=${NAME}-${VERSION} 7 | 8 | curl -L ${URL_REPO}/archive/v${VERSION}.tar.gz -o ${NAME}_${VERSION}.orig.tar.gz 9 | mkdir ${BUILD_DIR} 10 | tar -xzf ${NAME}_${VERSION}.orig.tar.gz -C ${BUILD_DIR} --strip-components 1 11 | cp -r debian ${BUILD_DIR} 12 | cd ${BUILD_DIR} 13 | debuild --force-sign 14 | -------------------------------------------------------------------------------- /packaging/deb/debian/changelog: -------------------------------------------------------------------------------- 1 | pamoauth2device (0.1.1-1) UNRELEASED; urgency=low 2 | 3 | * New features: 4 | - Add username_attribute to config (#7) 5 | * Bug fixes: 6 | - Add client authentication to device endpoint (#6) 7 | 8 | -- Jaroslaw Surkont Thu, 21 Nov 2019 11:00:00 +0200 9 | 10 | pamoauth2device (0.1.0-1) UNRELEASED; urgency=low 11 | 12 | * Initial release 13 | 14 | -- Jaroslaw Surkont Thu, 08 Aug 2019 10:10:42 +0200 15 | -------------------------------------------------------------------------------- /packaging/deb/debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /packaging/deb/debian/control: -------------------------------------------------------------------------------- 1 | Source: pamoauth2device 2 | Section: unknown 3 | Priority: optional 4 | Maintainer: Jaroslaw Surkont 5 | Build-Depends: debhelper (>= 10), 6 | libcurl4-openssl-dev, 7 | libldap2-dev, 8 | libpam0g-dev 9 | Standards-Version: 4.1.2 10 | Homepage: https://github.com/ICS-MU/pam_oauth2_device/tree/c_implementation 11 | 12 | Package: pamoauth2device 13 | Architecture: any 14 | Depends: ${shlibs:Depends}, ${misc:Depends}, 15 | curl, 16 | ldap-utils 17 | Description: PAM module for OAuth 2.0 Device Flow 18 | PAM module that allows authentication against external OpenID Connect 19 | identity provider using OAuth 2.0 Device Flow. 20 | -------------------------------------------------------------------------------- /packaging/deb/debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: pamoauth2device 3 | Source: https://github.com/ICS-MU/pam_oauth2_device/tree/c_implementation 4 | 5 | Files: * 6 | Copyright: 2019 Jaroslaw Surkont 7 | License: Apache-2.0 8 | 9 | Files: src/include/nayuki/* 10 | Copyright: Project Nayuki 11 | License: MIT 12 | 13 | Files: src/include/nlohmann/* 14 | Copyright: 2013-2018 Niels Lohmann 15 | License: MIT 16 | 17 | License: Apache-2.0 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | You may obtain a copy of the License at 21 | . 22 | https://www.apache.org/licenses/LICENSE-2.0 23 | . 24 | Unless required by applicable law or agreed to in writing, software 25 | distributed under the License is distributed on an "AS IS" BASIS, 26 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | See the License for the specific language governing permissions and 28 | limitations under the License. 29 | . 30 | On Debian systems, the complete text of the Apache version 2.0 license 31 | can be found in "/usr/share/common-licenses/Apache-2.0". 32 | 33 | License: MIT 34 | Permission is hereby granted, free of charge, to any person obtaining a 35 | copy of this software and associated documentation files (the "Software"), 36 | to deal in the Software without restriction, including without limitation 37 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 38 | and/or sell copies of the Software, and to permit persons to whom the 39 | Software is furnished to do so, subject to the following conditions: 40 | . 41 | The above copyright notice and this permission notice shall be included 42 | in all copies or substantial portions of the Software. 43 | . 44 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 45 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 46 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 47 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 48 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 49 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 50 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 51 | -------------------------------------------------------------------------------- /packaging/deb/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # See debhelper(7) (uncomment to enable) 3 | # output every command that modifies files on the build system. 4 | #export DH_VERBOSE = 1 5 | 6 | 7 | # see FEATURE AREAS in dpkg-buildflags(1) 8 | #export DEB_BUILD_MAINT_OPTIONS = hardening=+all 9 | 10 | # see ENVIRONMENT in dpkg-buildflags(1) 11 | # package maintainers to append CFLAGS 12 | #export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic 13 | # package maintainers to append LDFLAGS 14 | #export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed 15 | 16 | 17 | %: 18 | dh $@ 19 | 20 | 21 | # dh_make generated override targets 22 | # This is example for Cmake (See https://bugs.debian.org/641051 ) 23 | #override_dh_auto_configure: 24 | # dh_auto_configure -- # -DCMAKE_LIBRARY_PATH=$(DEB_HOST_MULTIARCH) 25 | 26 | -------------------------------------------------------------------------------- /packaging/deb/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /packaging/rpm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | 3 | RUN yum install -y \ 4 | gcc \ 5 | gcc-c++ \ 6 | libcurl-devel \ 7 | make \ 8 | openldap-devel \ 9 | pam-devel \ 10 | rpm-build \ 11 | && yum clean all 12 | 13 | RUN groupadd builder \ 14 | && useradd --create-home --gid builder builder 15 | 16 | USER builder 17 | 18 | # This version must be identical to the version in pamoauth2device.spec 19 | ENV PACKAGE_VERSION=0.1 20 | 21 | WORKDIR /home/builder 22 | 23 | RUN mkdir -p rpmbuild/SOURCES \ 24 | && cd rpmbuild/SOURCES \ 25 | && curl -L -O https://github.com/stfc/pam_oauth2_device/archive/v${PACKAGE_VERSION}.tar.gz 26 | 27 | COPY pamoauth2device.spec . 28 | 29 | USER root 30 | 31 | RUN chown builder:builder pamoauth2device.spec 32 | 33 | USER builder 34 | 35 | RUN rpmbuild -ba pamoauth2device.spec 36 | -------------------------------------------------------------------------------- /packaging/rpm/pamoauth2device.spec: -------------------------------------------------------------------------------- 1 | # pam_oauth2_device version 2 | %define _version 0.1 3 | %define _lib /lib64 4 | 5 | 6 | Name: pamoauth2device 7 | Version: %{_version} 8 | Release: 1%{?dist} 9 | Summary: PAM module for OAuth 2.0 Device flow 10 | License: Apache-2.0 11 | URL: https://github.com/stfc/pam_oauth2_device/ 12 | Source0: https://github.com/stfc/pam_oauth2_device/archive/v%{_version}.tar.gz 13 | 14 | 15 | # List of build-time dependencies: 16 | BuildRequires: gcc 17 | BuildRequires: gcc-c++ 18 | BuildRequires: make 19 | BuildRequires: libcurl-devel 20 | BuildRequires: openldap-devel 21 | BuildRequires: pam-devel 22 | 23 | 24 | # List of runtime dependencies: 25 | Requires: curl 26 | Requires: openldap-clients 27 | 28 | 29 | %description 30 | PAM module that allows authentication against external OpenID Connect 31 | identity provider using OAuth 2.0 Device Flow. 32 | 33 | 34 | %prep 35 | %setup -q -n pam_oauth2_device-%{_version} 36 | 37 | 38 | %build 39 | make 40 | 41 | 42 | %install 43 | mkdir -p ${RPM_BUILD_ROOT}%{_lib}/security 44 | mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/pam_oauth2_device 45 | install pam_oauth2_device.so ${RPM_BUILD_ROOT}%{_lib}/security 46 | cp config_template.json ${RPM_BUILD_ROOT}%{_sysconfdir}/pam_oauth2_device/config.json 47 | 48 | 49 | %check 50 | # no test. 51 | 52 | 53 | %files 54 | %doc LICENSE README.md 55 | %{_lib}/security/pam_oauth2_device.so 56 | %{_sysconfdir}/pam_oauth2_device/config.json 57 | 58 | 59 | %changelog 60 | * Thu Aug 13 2020 Will Furnell - 0.1 61 | - Revamped completely for STFC use 62 | 63 | * Thu Nov 21 2019 Jaroslaw Surkont - 0.1.1-1 64 | - Add username_attribute to config (#7) 65 | - Add client authentication to device endpoint (#6) 66 | 67 | * Fri Aug 09 2019 Jaroslaw Surkont - 0.1.0-1 68 | - first build for pamoauth2device. 69 | -------------------------------------------------------------------------------- /src/include/config.cpp: -------------------------------------------------------------------------------- 1 | /***** Load the configuration *****/ 2 | /* (C) 2022 UKRI-STFC 3 | * https://www.iris.ac.uk/ 4 | * Jens Jensen, UKRI-STFC 5 | * 6 | * Written in C++11 to match the rest of the project 7 | * https://github.com/stfc/pam_oauth2_device 8 | */ 9 | 10 | 11 | #include "nlohmann/json.hpp" // TODO: use the system one 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include "config.hpp" 21 | 22 | 23 | // currently a compile time constant 24 | constexpr bool debug = false; 25 | 26 | using json = nlohmann::json; 27 | using list = std::forward_list; 28 | using iter = list::const_iterator; 29 | 30 | 31 | /** Return type 32 | * @brief we need to return things of different types, and without std::variant (C++17) we need to hack it ourselves 33 | */ 34 | class value final { 35 | public: 36 | enum class value_type { VT_ERR, VT_NULL, VT_STR, VT_INT, VT_BOOL }; 37 | 38 | value() : type_(value_type::VT_NULL), strval_(), intval_(), boolval_() { } 39 | 40 | value(std::string &&str) : type_(value_type::VT_STR), strval_(std::forward(str)), intval_(), boolval_() { } 41 | 42 | explicit value(char const *str) : type_(value_type::VT_STR), strval_(str), intval_(), boolval_() { } 43 | 44 | explicit value(int k) : type_(value_type::VT_INT), strval_(), intval_(k), boolval_() { } 45 | 46 | explicit value(bool t) : type_(value_type::VT_BOOL), strval_(), intval_(), boolval_(t) { } 47 | 48 | [[nodiscard]] value_type type() const noexcept { return type_; } 49 | 50 | /** Construct a value based on the JSON structure assuming it is one of those we can encode */ 51 | value(json const &); 52 | 53 | std::string &&get_str() && 54 | { 55 | assert(type_ == value_type::VT_STR); 56 | return std::forward(strval_); 57 | } 58 | std::string get_str() const & 59 | { 60 | assert(type_ == value_type::VT_STR); 61 | return strval_; 62 | } 63 | int get_int() const 64 | { 65 | assert(type_ == value_type::VT_INT); 66 | return intval_; 67 | } 68 | int get_bool() const 69 | { 70 | assert(type_ == value_type::VT_BOOL); 71 | return boolval_; 72 | } 73 | private: 74 | value_type type_; 75 | // No point trying to do this as a union... 76 | std::string strval_; 77 | int intval_; 78 | bool boolval_; 79 | 80 | friend std::ostream &operator<<(std::ostream &, value const &); 81 | friend bool operator==(value const &, value const &); 82 | friend bool operator!=(value const &a, value const &b); 83 | }; 84 | 85 | 86 | // Tuple of (1) path, (2) default, (3) required 87 | using item = std::tuple; 88 | 89 | 90 | /** Slightly hacky class for transferring a value to a variable */ 91 | class variable final { 92 | private: 93 | item const place_; 94 | std::string *s_; 95 | int *i_; 96 | bool *b_; 97 | /** check and transfer value throwing exception if they have different types. 98 | * As regards const, see comment on set() 99 | */ 100 | void check_type(value const &, list const &) const; 101 | public: 102 | variable(list &&path, value &&def, bool reqd, std::string &s) : place_(std::forward(path), std::forward(def), reqd), s_(&s), i_(nullptr), b_(nullptr) {} 103 | variable(list &&path, value &&def, bool reqd, int &i) : place_(std::forward(path), std::forward(def), reqd), s_(nullptr), i_(&i), b_(nullptr) {} 104 | variable(list &&path, value &&def, bool reqd, bool &b) : place_(std::forward(path), std::forward(def), reqd), s_(nullptr), i_(nullptr), b_(&b) {} 105 | /** Set the variable directly from parsing the JSON structure. 106 | * This is constant from the object's perspective as we just hold a pointer to the target 107 | */ 108 | void set(json const &) const; 109 | }; 110 | 111 | 112 | /** Read a string attribute 113 | * @param j initial JSON structure 114 | * @param next iterator pointing to the current item we're looking for 115 | * @param end end value for iterator 116 | */ 117 | value load(json j, iter next, iter end, bool required); 118 | 119 | /** Read a file, returning its json structure */ 120 | json read_file(std::string const &filename); 121 | 122 | 123 | std::string print_list(list const &); 124 | 125 | 126 | 127 | /**** Principal Load Function ****/ 128 | 129 | 130 | void 131 | Config::load(const char *path) 132 | { 133 | json const config = read_file(path); 134 | auto const the_end = config.end(); 135 | 136 | // Everything but the 'users' section 137 | 138 | // TODO: this captures the optional attributes but loses the check that if a section is present, some of its attributes are mandatory 139 | // TODO: It also makes error messages less clear: scope occurs two places and the context may not be passed back 140 | // TODO: When distributed over several files, it is not clear which file raises the exception 141 | std::forward_list const configdata = \ 142 | { // {path, default, required, target_variable} 143 | variable({"oauth","client","id"}, value(""), true, client_id), 144 | variable({"oauth","client","secret"}, value(""), true, client_secret), 145 | variable({"oauth","scope"},value(""), true, scope), 146 | variable({"oauth","device_endpoint"},value(""), true, device_endpoint), 147 | variable({"oauth","token_endpoint"},value(""), true, token_endpoint), 148 | variable({"oauth","userinfo_endpoint"},value(""), true, userinfo_endpoint), 149 | variable({"oauth","username_attribute"},value(""),true, username_attribute), 150 | variable({"oauth","local_username_suffix"},value(""),false, local_username_suffix), 151 | variable({"qr","error_correction_level"},value(-1),false, qr_error_correction_level), 152 | variable({"client_debug"},value(false),false, client_debug), 153 | variable({"http_basic_auth"},value(true),false, http_basic_auth), 154 | variable({"cloud","access"},value(false),false, cloud_access), 155 | variable({"cloud","endpoint"},value(""),false, cloud_endpoint), 156 | variable({"cloud","username"},value(""),false, cloud_username), 157 | variable({"cloud","metadata_file"},value(""),false, metadata_file), 158 | variable({"tls","ca_bundle"},value(""),false, tls_ca_bundle), 159 | variable({"tls","ca_path"}, value("/etc/grid-security/certificates"),false, tls_ca_path), 160 | variable({"group","access"},value(false),false, group_access), 161 | variable({"group","service_name"},value(""),false, group_service_name), 162 | variable({"ldap","host"},value(""),false, ldap_host), 163 | variable({"ldap","basedn"},value(""),false, ldap_basedn), 164 | variable({"ldap","user"},value(""),false, ldap_user), 165 | variable({"ldap","passwd"},value(""),false, ldap_passwd), 166 | variable({"ldap","scope"},value(""),false, ldap_scope), 167 | variable({"ldap","preauth"},value(""),false, ldap_preauth), 168 | variable({"ldap","filter"},value(""),false, ldap_filter), 169 | variable({"ldap","attr"},value(""),false, ldap_attr) 170 | }; 171 | for( auto var : configdata ) 172 | var.set(config); 173 | 174 | // The users section is much like before but can be in a separate file, too 175 | if (config.find("users") != the_end) { 176 | json j = config.at("users"); 177 | if(j.type() == json::value_t::string) { 178 | auto const fn = j.get(); 179 | if(debug) 180 | std::cerr << "Read users from " << fn << '\n'; 181 | j = read_file(fn); 182 | } 183 | 184 | for (auto const &element : j.items()) 185 | { 186 | for (auto const &local_user : element.value()) 187 | { 188 | if (usermap.find(element.key()) == usermap.end()) 189 | { 190 | std::set userset; 191 | userset.insert((std::string)local_user); 192 | usermap[element.key()] = userset; 193 | if(debug) 194 | std::cerr << "CONF SET users NEW " << element.key() << " " << local_user << std::endl; 195 | } 196 | else 197 | { 198 | usermap[element.key()].insert((std::string)local_user); 199 | if(debug) 200 | std::cerr << "CONF SET users ADD " << element.key() << " " << local_user << std::endl; 201 | } 202 | } 203 | } 204 | } 205 | else if(debug) 206 | std::cerr << "No users section found\n"; 207 | 208 | } 209 | 210 | 211 | 212 | 213 | json 214 | read_file(std::string const &filename) 215 | { 216 | std::ifstream f(filename); 217 | if(!f.is_open()) { 218 | std::string computer_says_no = "File not found: "; 219 | computer_says_no += filename; 220 | throw std::runtime_error(computer_says_no); 221 | } 222 | json fred; 223 | f >> fred; 224 | return fred; 225 | } 226 | 227 | 228 | value 229 | load(json j, iter next, iter end, bool required) 230 | { 231 | if(debug) { 232 | if(next != end) 233 | std::cerr << "load " << *next << " from " << j << std::endl; 234 | else 235 | std::cerr << "retn " << j << std::endl; 236 | } 237 | switch(j.type()) { 238 | case json::value_t::string: 239 | { 240 | std::string val = j.get(); 241 | if(next == end) 242 | return value(std::move(val)); 243 | // The current entry is a filename 244 | return load(read_file(val), next, end, required); 245 | } 246 | case json::value_t::number_integer: 247 | case json::value_t::number_unsigned: 248 | case json::value_t::boolean: 249 | return value(j); 250 | case json::value_t::object: 251 | // We've reached the target but it's an object 252 | if(next == end) 253 | throw "config - reference returns object"; 254 | if(required) { 255 | auto node = j.at(*next); 256 | return load(node, std::next(next), end, required); 257 | } else { 258 | auto node = j.find(*next); 259 | if(node != j.end()) 260 | return load(j.at(*next), std::next(next), end, required); 261 | return value(); 262 | } 263 | default: 264 | std::ostringstream msg; 265 | msg << "Unsupported JSON type: " << j; 266 | throw std::runtime_error(msg.str()); 267 | } 268 | return value(); // can't happen 269 | } 270 | 271 | 272 | bool 273 | operator!=(value const &a, value const &b) 274 | { 275 | return !(a == b); 276 | } 277 | 278 | 279 | bool 280 | operator==(value const &a, value const &b) 281 | { 282 | if(a.type_ != b.type_) 283 | return false; 284 | switch(a.type_) { 285 | case value::value_type::VT_ERR: 286 | case value::value_type::VT_NULL: 287 | return true; 288 | case value::value_type::VT_STR: 289 | return a.strval_ == b.strval_; 290 | case value::value_type::VT_INT: 291 | return a.intval_ == b.intval_; 292 | case value::value_type::VT_BOOL: 293 | return a.boolval_ == b.boolval_; 294 | } 295 | throw std::runtime_error("Unknown value types"); // can't happen 296 | } 297 | 298 | 299 | std::string 300 | print_list(list const &l) 301 | { 302 | std::ostringstream os; 303 | for(auto const s : l) 304 | os << '/' << s; 305 | return os.str(); 306 | } 307 | 308 | 309 | value::value(json const &j) : type_(value_type::VT_ERR), strval_(), intval_(), boolval_() 310 | { 311 | switch(j.type()) { 312 | case json::value_t::null: 313 | type_ = value_type::VT_NULL; 314 | break; 315 | case json::value_t::boolean: 316 | type_ = value_type::VT_BOOL; 317 | boolval_ = j.get(); 318 | break; 319 | case json::value_t::string: 320 | type_ = value_type::VT_STR; 321 | strval_ = j.get(); 322 | break; 323 | case json::value_t::number_integer: 324 | type_ = value_type::VT_INT; 325 | // check for overflow? 326 | intval_ = j.get(); 327 | break; 328 | case json::value_t::number_unsigned: 329 | { 330 | unsigned int val = j.get(); 331 | type_ = value_type::VT_INT; 332 | // check for overflow? 333 | intval_ = static_cast(val); 334 | break; 335 | } 336 | default: 337 | { 338 | std::ostringstream os; 339 | os << "Cannot construct value from " << j; 340 | throw std::runtime_error(os.str()); 341 | } 342 | } 343 | } 344 | 345 | 346 | std::ostream & 347 | operator<<(std::ostream &os, value const &v) 348 | { 349 | switch(v.type_) { 350 | case value::value_type::VT_ERR: 351 | os << "ERR"; 352 | break; 353 | case value::value_type::VT_NULL: 354 | os << "NULL"; 355 | break; 356 | case value::value_type::VT_STR: 357 | os << v.strval_; 358 | break; 359 | case value::value_type::VT_INT: 360 | os << v.intval_; 361 | break; 362 | case value::value_type::VT_BOOL: 363 | os << (v.boolval_ ? "true" : "false"); 364 | break; 365 | } 366 | return os; 367 | } 368 | 369 | 370 | void 371 | variable::set(json const &j) const 372 | { 373 | list const &path(std::get<0>(place_)); 374 | value val = load(j, path.cbegin(), path.cend(), std::get<2>(place_)); 375 | switch(val.type()) { 376 | case value::value_type::VT_ERR: 377 | { 378 | std::ostringstream os; 379 | os << "Failed to read " << print_list(path); 380 | throw std::runtime_error(os.str()); 381 | } 382 | case value::value_type::VT_NULL: 383 | if(std::get<2>(place_)) { 384 | std::ostringstream os; 385 | os << "Required value for " << print_list(path) << " missing"; 386 | throw std::runtime_error(os.str()); 387 | } 388 | // Use the default value 389 | check_type(std::get<1>(place_), path); 390 | return; 391 | default: 392 | // All remaining types 393 | check_type(val, path); 394 | } 395 | } 396 | 397 | 398 | void 399 | variable::check_type(value const &v, list const &path) const 400 | { 401 | char const *type = "unknown"; 402 | if(debug) 403 | std::cerr << "CONF SET " << print_list(std::get<0>(place_)) << " TO "; 404 | switch(v.type()) { 405 | case value::value_type::VT_ERR: 406 | case value::value_type::VT_NULL: 407 | { 408 | std::ostringstream os; 409 | os << "Attempt to set" << print_list(path) << " to null/error"; 410 | if(debug) 411 | std::cerr << "NULL/ERR\n"; 412 | throw std::runtime_error(os.str()); 413 | } 414 | case value::value_type::VT_STR: 415 | if(s_) { 416 | *s_ = v.get_str(); 417 | if(debug) 418 | std::cerr << *s_ << std::endl; 419 | return; 420 | } 421 | type = "string"; 422 | break; 423 | case value::value_type::VT_INT: 424 | if(i_) { 425 | *i_ = v.get_int(); 426 | if(debug) 427 | std::cerr << *i_ << std::endl; 428 | return; 429 | } 430 | type = "int"; 431 | break; 432 | case value::value_type::VT_BOOL: 433 | if(b_) { 434 | *b_ = v.get_bool(); 435 | if(debug) 436 | std::cerr << (*b_ ? "true" : "false") << std::endl; 437 | return; 438 | } 439 | type = "bool"; 440 | break; 441 | } 442 | // type mismatch 443 | std::ostringstream exc; 444 | exc << print_list(path) << ": expected "; 445 | if(s_) exc << "string"; 446 | if(i_) exc << "int"; 447 | if(b_) exc << "bool"; 448 | exc << " got " << type; 449 | throw std::runtime_error(exc.str()); 450 | } 451 | 452 | -------------------------------------------------------------------------------- /src/include/config.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PAM_OAUTH2_DEVICE_CONFIG_HPP 2 | #define PAM_OAUTH2_DEVICE_CONFIG_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class Config 9 | { 10 | public: 11 | void load(const char *path); 12 | std::string client_id, 13 | client_secret, 14 | scope, 15 | device_endpoint, 16 | token_endpoint, 17 | userinfo_endpoint, 18 | username_attribute, 19 | ldap_host, 20 | ldap_basedn, 21 | ldap_scope, 22 | ldap_user, 23 | ldap_passwd, 24 | ldap_preauth, 25 | ldap_filter, 26 | ldap_attr, 27 | tls_ca_path, 28 | tls_ca_bundle, 29 | group_service_name, 30 | cloud_endpoint, 31 | cloud_username, 32 | local_username_suffix, 33 | metadata_file; 34 | int qr_error_correction_level; 35 | bool group_access, 36 | cloud_access, 37 | group_and_username_access, 38 | http_basic_auth, 39 | client_debug; 40 | std::map> usermap; 41 | }; 42 | 43 | #endif // PAM_OAUTH2_DEVICE_CONFIG_HPP 44 | -------------------------------------------------------------------------------- /src/include/ldapquery.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "ldapquery.h" 7 | #include 8 | #include 9 | 10 | 11 | 12 | int 13 | ldap_scope_value(char const *string) 14 | { 15 | // Some sort of sensible default for an empty input 16 | if(!string || !*string) 17 | return LDAP_SCOPE_SUB; 18 | if(!strcasecmp(string, "base") || !strcasecmp(string, "baseobject")) 19 | return LDAP_SCOPE_BASE; 20 | if(!strcasecmp(string, "one") || !strcasecmp(string, "onelevel")) 21 | return LDAP_SCOPE_ONE; 22 | if(!strcasecmp(string, "sub") || !strcasecmp(string, "subtree")) 23 | return LDAP_SCOPE_SUB; 24 | // OpenLDAP extension 25 | #ifdef LDAP_SCOPE_SUBORDINATE 26 | if(!strcasecmp(string, "subordinate") || !strcasecmp(string, "children")) 27 | return LDAP_SCOPE_SUBORDINATE; 28 | #endif 29 | return -1000; 30 | } 31 | 32 | 33 | 34 | int ldap_check_attr(void const *pamh, enum ldap_loglevel_t log, 35 | const char *host, const char *basedn, int scope, 36 | const char *user, const char *passwd, 37 | const char *filter, const char *attr, 38 | const char *value) { 39 | LDAP *ld; 40 | LDAPMessage *res, *msg; 41 | BerElement *ber; 42 | BerValue *servercredp; 43 | char *a, *passwd_local; 44 | int rc, i; 45 | struct berval cred; 46 | struct berval **vals; 47 | char *attr_local = NULL; 48 | char *attrs[] = {attr_local, NULL}; 49 | const int ldap_version = LDAP_VERSION3; 50 | 51 | if (ldap_initialize(&ld, host) != LDAP_SUCCESS) { 52 | if(log != LDAP_LOGLEVEL_OFF) 53 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_ERR, "LDAP failed init"); 54 | return LDAPQUERY_ERROR; 55 | } 56 | 57 | if (ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ldap_version) != LDAP_SUCCESS) { 58 | if(log != LDAP_LOGLEVEL_OFF) 59 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_ERR, "LDAP failed to set v3"); 60 | return LDAPQUERY_ERROR; 61 | } 62 | 63 | if(user && *user && passwd && *passwd) { 64 | if(log == LDAP_LOGLEVEL_DEBUG) 65 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_DEBUG, "LDAP set user %s", user); 66 | passwd_local = (char *) malloc(strlen(passwd) + 1); 67 | if(!passwd_local) { 68 | if(log != LDAP_LOGLEVEL_OFF) 69 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_ERR, "LDAP malloc failed"); 70 | return LDAPQUERY_ERROR; 71 | } 72 | strcpy(passwd_local, passwd); 73 | cred.bv_val = passwd_local; 74 | cred.bv_len = strlen(passwd); 75 | rc = ldap_sasl_bind_s(ld, user, LDAP_SASL_SIMPLE, &cred, NULL, NULL, &servercredp); 76 | free(passwd_local); 77 | if (rc != LDAP_SUCCESS) { 78 | if(log != LDAP_LOGLEVEL_OFF) 79 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_ERR, "LDAP bind error (search): %s", ldap_err2string(rc)); 80 | return LDAPQUERY_ERROR; 81 | } 82 | } else { 83 | if(log <= LDAP_LOGLEVEL_INFO) 84 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_INFO, "LDAP search anonymous"); 85 | user = NULL; // quick flag to remind ourselves not to unbind 86 | } 87 | 88 | attr_local = strdup(attr); 89 | if(log == LDAP_LOGLEVEL_DEBUG) { 90 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_DEBUG, "Search base=%s scope=%d filter=%s attr=%s", basedn, scope, filter, attrs[0]); 91 | } 92 | rc = ldap_search_ext_s(ld, basedn, scope, filter, attrs, 0, NULL, NULL, NULL, 0, &res); 93 | free(attr_local); 94 | if (rc != LDAP_SUCCESS) { 95 | ldap_msgfree(res); 96 | if(user) 97 | ldap_unbind_ext_s(ld, NULL, NULL); 98 | if(log != LDAP_LOGLEVEL_OFF) 99 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_ERR, "LDAP search for %s failed: %s", attr, ldap_err2string(rc)); 100 | return LDAPQUERY_ERROR; 101 | } 102 | 103 | rc = LDAPQUERY_FALSE; 104 | for ( msg = ldap_first_message( ld, res ); msg != NULL; msg = ldap_next_message( ld, msg ) ) { 105 | switch(ldap_msgtype(msg)) { 106 | case LDAP_RES_SEARCH_ENTRY: 107 | for (a = ldap_first_attribute( ld, res, &ber ); a != NULL; a = ldap_next_attribute( ld, res, ber )) { 108 | if ((vals = ldap_get_values_len( ld, res, a)) != NULL) { 109 | for (i = 0; vals[i] != NULL; ++i) { 110 | if (strcmp(a, attr) == 0) { 111 | if (strcmp(vals[i]->bv_val, value) == 0) { 112 | rc = LDAPQUERY_TRUE; 113 | } 114 | } 115 | } 116 | ldap_value_free_len(vals); 117 | } 118 | ldap_memfree(a); 119 | } 120 | if (ber != NULL) { 121 | ber_free(ber, 0); 122 | } 123 | break; 124 | default: 125 | break; 126 | } 127 | } 128 | 129 | ldap_msgfree(res); 130 | if(user) 131 | ldap_unbind_ext_s(ld, NULL, NULL); 132 | return rc; 133 | } 134 | 135 | 136 | 137 | int 138 | ldap_bool_query(void const *pamh, enum ldap_loglevel_t log, 139 | const char *host, const char *basedn, int scope, 140 | const char *user, const char *passwd, 141 | const char *query) 142 | { 143 | LDAP *ld; 144 | LDAPMessage *res, *msg; 145 | char *passwd_local; 146 | int rc; 147 | BerValue *servercredp = NULL; 148 | struct berval cred; 149 | char *attrs[] = {LDAP_NO_ATTRS, NULL}; 150 | const int ldap_version = LDAP_VERSION3; 151 | 152 | if (ldap_initialize(&ld, host) != LDAP_SUCCESS) { 153 | if(log != LDAP_LOGLEVEL_OFF) 154 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_ERR, "Failed to init LDAP library"); 155 | return LDAPQUERY_ERROR; 156 | } 157 | 158 | if (ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ldap_version) != LDAP_SUCCESS) { 159 | if(log != LDAP_LOGLEVEL_OFF) 160 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_ERR, "Failed to set version 3"); 161 | return LDAPQUERY_ERROR; 162 | } 163 | 164 | if(user && *user && passwd && *passwd) { 165 | if(log == LDAP_LOGLEVEL_DEBUG) 166 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_DEBUG, "Setting LDAP user %s", user); 167 | passwd_local = (char *) malloc(strlen(passwd) + 1); 168 | if(!passwd_local) { 169 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_ERR, "LDAP malloc failed"); 170 | return LDAPQUERY_ERROR; 171 | } 172 | strcpy(passwd_local, passwd); 173 | cred.bv_val = passwd_local; 174 | cred.bv_len = strlen(passwd); 175 | rc = ldap_sasl_bind_s(ld, user, LDAP_SASL_SIMPLE, &cred, NULL, NULL, &servercredp); 176 | free(passwd_local); 177 | if (rc != LDAP_SUCCESS) { 178 | if(log != LDAP_LOGLEVEL_OFF) 179 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_ERR, "LDAP bind error (query): %s", ldap_err2string(rc)); 180 | return LDAPQUERY_ERROR; 181 | } 182 | } else { 183 | if(log <= LDAP_LOGLEVEL_INFO) 184 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_INFO, "LDAP query anonymous"); 185 | user = NULL; // quick hack to tell rest of code not to bother to unbind 186 | } 187 | 188 | if(log == LDAP_LOGLEVEL_DEBUG) { 189 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_DEBUG, "Query base=%s scope=%d query=%s", basedn, scope, query); 190 | } 191 | rc = ldap_search_ext_s(ld, basedn, scope, query, attrs, 1, NULL, NULL, NULL, 0, &res); 192 | if (rc != LDAP_SUCCESS) { 193 | ldap_msgfree(res); 194 | if(user) 195 | ldap_unbind_ext_s(ld, NULL, NULL); 196 | if(log != LDAP_LOGLEVEL_OFF) 197 | pam_syslog(pamh, LOG_AUTHPRIV|LOG_ERR, "LDAP query error: %s", ldap_err2string(rc)); 198 | return LDAPQUERY_ERROR; 199 | } 200 | 201 | rc = LDAPQUERY_FALSE; // reusing rc 202 | msg = ldap_first_message(ld, res); 203 | while(msg) { 204 | // We are just looking for a search entry, meaning the query returned something 205 | if(ldap_msgtype(msg) == LDAP_RES_SEARCH_ENTRY) 206 | rc = LDAPQUERY_TRUE; 207 | msg = ldap_next_message(ld, msg); 208 | } 209 | ldap_msgfree(res); 210 | if(user) 211 | ldap_unbind_ext_s(ld, NULL, NULL); 212 | return rc; 213 | } 214 | -------------------------------------------------------------------------------- /src/include/ldapquery.h: -------------------------------------------------------------------------------- 1 | #ifndef PAM_OAUTH2_DEVICE_LDAPQUERY_H 2 | #define PAM_OAUTH2_DEVICE_LDAPQUERY_H 3 | 4 | #define LDAPQUERY_ERROR -1 5 | #define LDAPQUERY_TRUE 1 6 | #define LDAPQUERY_FALSE 0 7 | 8 | 9 | #ifdef __cplusplus 10 | extern "C" { 11 | #endif 12 | 13 | // Translating the C++ enum class defined in pam_oauth2_log into C 14 | enum ldap_loglevel_t { LDAP_LOGLEVEL_DEBUG, LDAP_LOGLEVEL_INFO, LDAP_LOGLEVEL_WARN, LDAP_LOGLEVEL_ERR, LDAP_LOGLEVEL_OFF }; 15 | 16 | 17 | //! Convert a string value to an LDAP scope (which should be ber_int_t) 18 | //! (ie must be one of "base", "subtree", "onelevel", or "children", or a few common variations thereof) 19 | //! On failure, returns -1000 20 | int ldap_scope_value(char const *); 21 | 22 | //! Query an LDAP server for attr, which should match value (case sensitively) 23 | //! pamh is used for logging 24 | int ldap_check_attr(void const *pamh, enum ldap_loglevel_t log, 25 | const char *host, const char *basedn, int scope, 26 | const char *user, const char *passwd, 27 | const char *filter, const char *attr, 28 | const char *value); 29 | 30 | //! Run a query against an LDAP server and see whether it is successful 31 | //! pamh is used for logging 32 | int ldap_bool_query(void const *pamh, enum ldap_loglevel_t log, 33 | const char *host, const char *basedn, int scope, 34 | const char *user, const char *passwd, 35 | const char *query); 36 | 37 | 38 | #ifdef __cplusplus 39 | } 40 | #endif 41 | 42 | #endif // PAM_OAUTH2_DEVICE_LDAPQUERY_H 43 | -------------------------------------------------------------------------------- /src/include/metadata.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "metadata.hpp" 4 | #include "nlohmann/json.hpp" 5 | 6 | using json = nlohmann::json; 7 | 8 | void Metadata::load(const char *path) 9 | { 10 | std::ifstream config_fstream(path); 11 | json j; 12 | config_fstream >> j; 13 | 14 | project_id = j.at("project_id").get(); 15 | } -------------------------------------------------------------------------------- /src/include/metadata.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PAM_OAUTH2_DEVICE_METADATA_HPP 2 | #define PAM_OAUTH2_DEVICE_METADATA_HPP 3 | 4 | #include 5 | 6 | class Metadata 7 | { 8 | public: 9 | void load(const char *path); 10 | std::string project_id; 11 | }; 12 | 13 | #endif // PAM_OAUTH2_DEVICE_METADATA_HPP -------------------------------------------------------------------------------- /src/include/nayuki/BitBuffer.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * QR Code generator library (C++) 3 | * 4 | * Copyright (c) Project Nayuki. (MIT License) 5 | * https://www.nayuki.io/page/qr-code-generator-library 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 of 11 | * the Software, and to permit persons to whom the Software is furnished to do so, 12 | * subject to the following conditions: 13 | * - The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 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 | 24 | #include 25 | #include "BitBuffer.hpp" 26 | 27 | 28 | namespace qrcodegen { 29 | 30 | BitBuffer::BitBuffer() 31 | : std::vector() {} 32 | 33 | 34 | void BitBuffer::appendBits(std::uint32_t val, int len) { 35 | if (len < 0 || len > 31 || val >> len != 0) 36 | throw std::domain_error("Value out of range"); 37 | for (int i = len - 1; i >= 0; i--) // Append bit by bit 38 | this->push_back(((val >> i) & 1) != 0); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/include/nayuki/BitBuffer.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * QR Code generator library (C++) 3 | * 4 | * Copyright (c) Project Nayuki. (MIT License) 5 | * https://www.nayuki.io/page/qr-code-generator-library 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 of 11 | * the Software, and to permit persons to whom the Software is furnished to do so, 12 | * subject to the following conditions: 13 | * - The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 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 | 24 | #pragma once 25 | 26 | #include 27 | #include 28 | 29 | 30 | namespace qrcodegen { 31 | 32 | /* 33 | * An appendable sequence of bits (0s and 1s). Mainly used by QrSegment. 34 | */ 35 | class BitBuffer final : public std::vector { 36 | 37 | /*---- Constructor ----*/ 38 | 39 | // Creates an empty bit buffer (length 0). 40 | public: BitBuffer(); 41 | 42 | 43 | 44 | /*---- Method ----*/ 45 | 46 | // Appends the given number of low-order bits of the given value 47 | // to this buffer. Requires 0 <= len <= 31 and val < 2^len. 48 | public: void appendBits(std::uint32_t val, int len); 49 | 50 | }; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/include/nayuki/QrCode.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * QR Code generator library (C++) 3 | * 4 | * Copyright (c) Project Nayuki. (MIT License) 5 | * https://www.nayuki.io/page/qr-code-generator-library 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 of 11 | * the Software, and to permit persons to whom the Software is furnished to do so, 12 | * subject to the following conditions: 13 | * - The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 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 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include "BitBuffer.hpp" 31 | #include "QrCode.hpp" 32 | 33 | using std::int8_t; 34 | using std::uint8_t; 35 | using std::size_t; 36 | using std::vector; 37 | 38 | 39 | namespace qrcodegen { 40 | 41 | int QrCode::getFormatBits(Ecc ecl) { 42 | switch (ecl) { 43 | case Ecc::LOW : return 1; 44 | case Ecc::MEDIUM : return 0; 45 | case Ecc::QUARTILE: return 3; 46 | case Ecc::HIGH : return 2; 47 | default: throw std::logic_error("Assertion error"); 48 | } 49 | } 50 | 51 | 52 | QrCode QrCode::encodeText(const char *text, Ecc ecl) { 53 | vector segs = QrSegment::makeSegments(text); 54 | return encodeSegments(segs, ecl); 55 | } 56 | 57 | 58 | QrCode QrCode::encodeBinary(const vector &data, Ecc ecl) { 59 | vector segs{QrSegment::makeBytes(data)}; 60 | return encodeSegments(segs, ecl); 61 | } 62 | 63 | 64 | QrCode QrCode::encodeSegments(const vector &segs, Ecc ecl, 65 | int minVersion, int maxVersion, int mask, bool boostEcl) { 66 | if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7) 67 | throw std::invalid_argument("Invalid value"); 68 | 69 | // Find the minimal version number to use 70 | int version, dataUsedBits; 71 | for (version = minVersion; ; version++) { 72 | int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available 73 | dataUsedBits = QrSegment::getTotalBits(segs, version); 74 | if (dataUsedBits != -1 && dataUsedBits <= dataCapacityBits) 75 | break; // This version number is found to be suitable 76 | if (version >= maxVersion) { // All versions in the range could not fit the given data 77 | std::ostringstream sb; 78 | if (dataUsedBits == -1) 79 | sb << "Segment too long"; 80 | else { 81 | sb << "Data length = " << dataUsedBits << " bits, "; 82 | sb << "Max capacity = " << dataCapacityBits << " bits"; 83 | } 84 | throw data_too_long(sb.str()); 85 | } 86 | } 87 | if (dataUsedBits == -1) 88 | throw std::logic_error("Assertion error"); 89 | 90 | // Increase the error correction level while the data still fits in the current version number 91 | for (Ecc newEcl : vector{Ecc::MEDIUM, Ecc::QUARTILE, Ecc::HIGH}) { // From low to high 92 | if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8) 93 | ecl = newEcl; 94 | } 95 | 96 | // Concatenate all segments to create the data bit string 97 | BitBuffer bb; 98 | for (const QrSegment &seg : segs) { 99 | bb.appendBits(seg.getMode().getModeBits(), 4); 100 | bb.appendBits(seg.getNumChars(), seg.getMode().numCharCountBits(version)); 101 | bb.insert(bb.end(), seg.getData().begin(), seg.getData().end()); 102 | } 103 | if (bb.size() != static_cast(dataUsedBits)) 104 | throw std::logic_error("Assertion error"); 105 | 106 | // Add terminator and pad up to a byte if applicable 107 | size_t dataCapacityBits = getNumDataCodewords(version, ecl) * 8; 108 | if (bb.size() > dataCapacityBits) 109 | throw std::logic_error("Assertion error"); 110 | bb.appendBits(0, std::min(4, dataCapacityBits - bb.size())); 111 | bb.appendBits(0, (8 - bb.size() % 8) % 8); 112 | if (bb.size() % 8 != 0) 113 | throw std::logic_error("Assertion error"); 114 | 115 | // Pad with alternating bytes until data capacity is reached 116 | for (uint8_t padByte = 0xEC; bb.size() < dataCapacityBits; padByte ^= 0xEC ^ 0x11) 117 | bb.appendBits(padByte, 8); 118 | 119 | // Pack bits into bytes in big endian 120 | vector dataCodewords(bb.size() / 8); 121 | for (size_t i = 0; i < bb.size(); i++) 122 | dataCodewords[i >> 3] |= (bb.at(i) ? 1 : 0) << (7 - (i & 7)); 123 | 124 | // Create the QR Code object 125 | return QrCode(version, ecl, dataCodewords, mask); 126 | } 127 | 128 | 129 | QrCode::QrCode(int ver, Ecc ecl, const vector &dataCodewords, int mask) : 130 | // Initialize fields and check arguments 131 | version(ver), 132 | errorCorrectionLevel(ecl) { 133 | if (ver < MIN_VERSION || ver > MAX_VERSION) 134 | throw std::domain_error("Version value out of range"); 135 | if (mask < -1 || mask > 7) 136 | throw std::domain_error("Mask value out of range"); 137 | size = ver * 4 + 17; 138 | modules = vector >(size, vector(size)); // Initially all white 139 | isFunction = vector >(size, vector(size)); 140 | 141 | // Compute ECC, draw modules 142 | drawFunctionPatterns(); 143 | const vector allCodewords = addEccAndInterleave(dataCodewords); 144 | drawCodewords(allCodewords); 145 | 146 | // Do masking 147 | if (mask == -1) { // Automatically choose best mask 148 | long minPenalty = LONG_MAX; 149 | for (int i = 0; i < 8; i++) { 150 | applyMask(i); 151 | drawFormatBits(i); 152 | long penalty = getPenaltyScore(); 153 | if (penalty < minPenalty) { 154 | mask = i; 155 | minPenalty = penalty; 156 | } 157 | applyMask(i); // Undoes the mask due to XOR 158 | } 159 | } 160 | if (mask < 0 || mask > 7) 161 | throw std::logic_error("Assertion error"); 162 | this->mask = mask; 163 | applyMask(mask); // Apply the final choice of mask 164 | drawFormatBits(mask); // Overwrite old format bits 165 | 166 | isFunction.clear(); 167 | isFunction.shrink_to_fit(); 168 | } 169 | 170 | 171 | int QrCode::getVersion() const { 172 | return version; 173 | } 174 | 175 | 176 | int QrCode::getSize() const { 177 | return size; 178 | } 179 | 180 | 181 | QrCode::Ecc QrCode::getErrorCorrectionLevel() const { 182 | return errorCorrectionLevel; 183 | } 184 | 185 | 186 | int QrCode::getMask() const { 187 | return mask; 188 | } 189 | 190 | 191 | bool QrCode::getModule(int x, int y) const { 192 | return 0 <= x && x < size && 0 <= y && y < size && module(x, y); 193 | } 194 | 195 | 196 | std::string QrCode::toSvgString(int border) const { 197 | if (border < 0) 198 | throw std::domain_error("Border must be non-negative"); 199 | if (border > INT_MAX / 2 || border * 2 > INT_MAX - size) 200 | throw std::overflow_error("Border too large"); 201 | 202 | std::ostringstream sb; 203 | sb << "\n"; 204 | sb << "\n"; 205 | sb << "\n"; 207 | sb << "\t\n"; 208 | sb << "\t\n"; 219 | sb << "\n"; 220 | return sb.str(); 221 | } 222 | 223 | 224 | void QrCode::drawFunctionPatterns() { 225 | // Draw horizontal and vertical timing patterns 226 | for (int i = 0; i < size; i++) { 227 | setFunctionModule(6, i, i % 2 == 0); 228 | setFunctionModule(i, 6, i % 2 == 0); 229 | } 230 | 231 | // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules) 232 | drawFinderPattern(3, 3); 233 | drawFinderPattern(size - 4, 3); 234 | drawFinderPattern(3, size - 4); 235 | 236 | // Draw numerous alignment patterns 237 | const vector alignPatPos = getAlignmentPatternPositions(); 238 | int numAlign = alignPatPos.size(); 239 | for (int i = 0; i < numAlign; i++) { 240 | for (int j = 0; j < numAlign; j++) { 241 | // Don't draw on the three finder corners 242 | if (!((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0))) 243 | drawAlignmentPattern(alignPatPos.at(i), alignPatPos.at(j)); 244 | } 245 | } 246 | 247 | // Draw configuration data 248 | drawFormatBits(0); // Dummy mask value; overwritten later in the constructor 249 | drawVersion(); 250 | } 251 | 252 | 253 | void QrCode::drawFormatBits(int mask) { 254 | // Calculate error correction code and pack bits 255 | int data = getFormatBits(errorCorrectionLevel) << 3 | mask; // errCorrLvl is uint2, mask is uint3 256 | int rem = data; 257 | for (int i = 0; i < 10; i++) 258 | rem = (rem << 1) ^ ((rem >> 9) * 0x537); 259 | int bits = (data << 10 | rem) ^ 0x5412; // uint15 260 | if (bits >> 15 != 0) 261 | throw std::logic_error("Assertion error"); 262 | 263 | // Draw first copy 264 | for (int i = 0; i <= 5; i++) 265 | setFunctionModule(8, i, getBit(bits, i)); 266 | setFunctionModule(8, 7, getBit(bits, 6)); 267 | setFunctionModule(8, 8, getBit(bits, 7)); 268 | setFunctionModule(7, 8, getBit(bits, 8)); 269 | for (int i = 9; i < 15; i++) 270 | setFunctionModule(14 - i, 8, getBit(bits, i)); 271 | 272 | // Draw second copy 273 | for (int i = 0; i < 8; i++) 274 | setFunctionModule(size - 1 - i, 8, getBit(bits, i)); 275 | for (int i = 8; i < 15; i++) 276 | setFunctionModule(8, size - 15 + i, getBit(bits, i)); 277 | setFunctionModule(8, size - 8, true); // Always black 278 | } 279 | 280 | 281 | void QrCode::drawVersion() { 282 | if (version < 7) 283 | return; 284 | 285 | // Calculate error correction code and pack bits 286 | int rem = version; // version is uint6, in the range [7, 40] 287 | for (int i = 0; i < 12; i++) 288 | rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); 289 | long bits = (long)version << 12 | rem; // uint18 290 | if (bits >> 18 != 0) 291 | throw std::logic_error("Assertion error"); 292 | 293 | // Draw two copies 294 | for (int i = 0; i < 18; i++) { 295 | bool bit = getBit(bits, i); 296 | int a = size - 11 + i % 3; 297 | int b = i / 3; 298 | setFunctionModule(a, b, bit); 299 | setFunctionModule(b, a, bit); 300 | } 301 | } 302 | 303 | 304 | void QrCode::drawFinderPattern(int x, int y) { 305 | for (int dy = -4; dy <= 4; dy++) { 306 | for (int dx = -4; dx <= 4; dx++) { 307 | int dist = std::max(std::abs(dx), std::abs(dy)); // Chebyshev/infinity norm 308 | int xx = x + dx, yy = y + dy; 309 | if (0 <= xx && xx < size && 0 <= yy && yy < size) 310 | setFunctionModule(xx, yy, dist != 2 && dist != 4); 311 | } 312 | } 313 | } 314 | 315 | 316 | void QrCode::drawAlignmentPattern(int x, int y) { 317 | for (int dy = -2; dy <= 2; dy++) { 318 | for (int dx = -2; dx <= 2; dx++) 319 | setFunctionModule(x + dx, y + dy, std::max(std::abs(dx), std::abs(dy)) != 1); 320 | } 321 | } 322 | 323 | 324 | void QrCode::setFunctionModule(int x, int y, bool isBlack) { 325 | modules.at(y).at(x) = isBlack; 326 | isFunction.at(y).at(x) = true; 327 | } 328 | 329 | 330 | bool QrCode::module(int x, int y) const { 331 | return modules.at(y).at(x); 332 | } 333 | 334 | 335 | vector QrCode::addEccAndInterleave(const vector &data) const { 336 | if (data.size() != static_cast(getNumDataCodewords(version, errorCorrectionLevel))) 337 | throw std::invalid_argument("Invalid argument"); 338 | 339 | // Calculate parameter numbers 340 | int numBlocks = NUM_ERROR_CORRECTION_BLOCKS[static_cast(errorCorrectionLevel)][version]; 341 | int blockEccLen = ECC_CODEWORDS_PER_BLOCK [static_cast(errorCorrectionLevel)][version]; 342 | int rawCodewords = getNumRawDataModules(version) / 8; 343 | int numShortBlocks = numBlocks - rawCodewords % numBlocks; 344 | int shortBlockLen = rawCodewords / numBlocks; 345 | 346 | // Split data into blocks and append ECC to each block 347 | vector > blocks; 348 | const ReedSolomonGenerator rs(blockEccLen); 349 | for (int i = 0, k = 0; i < numBlocks; i++) { 350 | vector dat(data.cbegin() + k, data.cbegin() + (k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1))); 351 | k += dat.size(); 352 | const vector ecc = rs.getRemainder(dat); 353 | if (i < numShortBlocks) 354 | dat.push_back(0); 355 | dat.insert(dat.end(), ecc.cbegin(), ecc.cend()); 356 | blocks.push_back(std::move(dat)); 357 | } 358 | 359 | // Interleave (not concatenate) the bytes from every block into a single sequence 360 | vector result; 361 | for (size_t i = 0; i < blocks.at(0).size(); i++) { 362 | for (size_t j = 0; j < blocks.size(); j++) { 363 | // Skip the padding byte in short blocks 364 | if (i != static_cast(shortBlockLen - blockEccLen) || j >= static_cast(numShortBlocks)) 365 | result.push_back(blocks.at(j).at(i)); 366 | } 367 | } 368 | if (result.size() != static_cast(rawCodewords)) 369 | throw std::logic_error("Assertion error"); 370 | return result; 371 | } 372 | 373 | 374 | void QrCode::drawCodewords(const vector &data) { 375 | if (data.size() != static_cast(getNumRawDataModules(version) / 8)) 376 | throw std::invalid_argument("Invalid argument"); 377 | 378 | size_t i = 0; // Bit index into the data 379 | // Do the funny zigzag scan 380 | for (int right = size - 1; right >= 1; right -= 2) { // Index of right column in each column pair 381 | if (right == 6) 382 | right = 5; 383 | for (int vert = 0; vert < size; vert++) { // Vertical counter 384 | for (int j = 0; j < 2; j++) { 385 | int x = right - j; // Actual x coordinate 386 | bool upward = ((right + 1) & 2) == 0; 387 | int y = upward ? size - 1 - vert : vert; // Actual y coordinate 388 | if (!isFunction.at(y).at(x) && i < data.size() * 8) { 389 | modules.at(y).at(x) = getBit(data.at(i >> 3), 7 - static_cast(i & 7)); 390 | i++; 391 | } 392 | // If this QR Code has any remainder bits (0 to 7), they were assigned as 393 | // 0/false/white by the constructor and are left unchanged by this method 394 | } 395 | } 396 | } 397 | if (i != data.size() * 8) 398 | throw std::logic_error("Assertion error"); 399 | } 400 | 401 | 402 | void QrCode::applyMask(int mask) { 403 | if (mask < 0 || mask > 7) 404 | throw std::domain_error("Mask value out of range"); 405 | for (int y = 0; y < size; y++) { 406 | for (int x = 0; x < size; x++) { 407 | bool invert; 408 | switch (mask) { 409 | case 0: invert = (x + y) % 2 == 0; break; 410 | case 1: invert = y % 2 == 0; break; 411 | case 2: invert = x % 3 == 0; break; 412 | case 3: invert = (x + y) % 3 == 0; break; 413 | case 4: invert = (x / 3 + y / 2) % 2 == 0; break; 414 | case 5: invert = x * y % 2 + x * y % 3 == 0; break; 415 | case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break; 416 | case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break; 417 | default: throw std::logic_error("Assertion error"); 418 | } 419 | modules.at(y).at(x) = modules.at(y).at(x) ^ (invert & !isFunction.at(y).at(x)); 420 | } 421 | } 422 | } 423 | 424 | 425 | long QrCode::getPenaltyScore() const { 426 | long result = 0; 427 | 428 | // Adjacent modules in row having same color, and finder-like patterns 429 | for (int y = 0; y < size; y++) { 430 | std::deque runHistory(7, 0); 431 | bool color = false; 432 | int runX = 0; 433 | for (int x = 0; x < size; x++) { 434 | if (module(x, y) == color) { 435 | runX++; 436 | if (runX == 5) 437 | result += PENALTY_N1; 438 | else if (runX > 5) 439 | result++; 440 | } else { 441 | addRunToHistory(runX, runHistory); 442 | if (!color && hasFinderLikePattern(runHistory)) 443 | result += PENALTY_N3; 444 | color = module(x, y); 445 | runX = 1; 446 | } 447 | } 448 | addRunToHistory(runX, runHistory); 449 | if (color) 450 | addRunToHistory(0, runHistory); // Dummy run of white 451 | if (hasFinderLikePattern(runHistory)) 452 | result += PENALTY_N3; 453 | } 454 | // Adjacent modules in column having same color, and finder-like patterns 455 | for (int x = 0; x < size; x++) { 456 | std::deque runHistory(7, 0); 457 | bool color = false; 458 | int runY = 0; 459 | for (int y = 0; y < size; y++) { 460 | if (module(x, y) == color) { 461 | runY++; 462 | if (runY == 5) 463 | result += PENALTY_N1; 464 | else if (runY > 5) 465 | result++; 466 | } else { 467 | addRunToHistory(runY, runHistory); 468 | if (!color && hasFinderLikePattern(runHistory)) 469 | result += PENALTY_N3; 470 | color = module(x, y); 471 | runY = 1; 472 | } 473 | } 474 | addRunToHistory(runY, runHistory); 475 | if (color) 476 | addRunToHistory(0, runHistory); // Dummy run of white 477 | if (hasFinderLikePattern(runHistory)) 478 | result += PENALTY_N3; 479 | } 480 | 481 | // 2*2 blocks of modules having same color 482 | for (int y = 0; y < size - 1; y++) { 483 | for (int x = 0; x < size - 1; x++) { 484 | bool color = module(x, y); 485 | if ( color == module(x + 1, y) && 486 | color == module(x, y + 1) && 487 | color == module(x + 1, y + 1)) 488 | result += PENALTY_N2; 489 | } 490 | } 491 | 492 | // Balance of black and white modules 493 | int black = 0; 494 | for (const vector &row : modules) { 495 | for (bool color : row) { 496 | if (color) 497 | black++; 498 | } 499 | } 500 | int total = size * size; // Note that size is odd, so black/total != 1/2 501 | // Compute the smallest integer k >= 0 such that (45-5k)% <= black/total <= (55+5k)% 502 | int k = static_cast((std::abs(black * 20L - total * 10L) + total - 1) / total) - 1; 503 | result += k * PENALTY_N4; 504 | return result; 505 | } 506 | 507 | 508 | vector QrCode::getAlignmentPatternPositions() const { 509 | if (version == 1) 510 | return vector(); 511 | else { 512 | int numAlign = version / 7 + 2; 513 | int step = (version == 32) ? 26 : 514 | (version*4 + numAlign*2 + 1) / (numAlign*2 - 2) * 2; 515 | vector result; 516 | for (int i = 0, pos = size - 7; i < numAlign - 1; i++, pos -= step) 517 | result.insert(result.begin(), pos); 518 | result.insert(result.begin(), 6); 519 | return result; 520 | } 521 | } 522 | 523 | 524 | int QrCode::getNumRawDataModules(int ver) { 525 | if (ver < MIN_VERSION || ver > MAX_VERSION) 526 | throw std::domain_error("Version number out of range"); 527 | int result = (16 * ver + 128) * ver + 64; 528 | if (ver >= 2) { 529 | int numAlign = ver / 7 + 2; 530 | result -= (25 * numAlign - 10) * numAlign - 55; 531 | if (ver >= 7) 532 | result -= 36; 533 | } 534 | return result; 535 | } 536 | 537 | 538 | int QrCode::getNumDataCodewords(int ver, Ecc ecl) { 539 | return getNumRawDataModules(ver) / 8 540 | - ECC_CODEWORDS_PER_BLOCK [static_cast(ecl)][ver] 541 | * NUM_ERROR_CORRECTION_BLOCKS[static_cast(ecl)][ver]; 542 | } 543 | 544 | 545 | void QrCode::addRunToHistory(int run, std::deque &history) { 546 | history.pop_back(); 547 | history.push_front(run); 548 | } 549 | 550 | 551 | bool QrCode::hasFinderLikePattern(const std::deque &runHistory) { 552 | int n = runHistory.at(1); 553 | return n > 0 && runHistory.at(2) == n && runHistory.at(4) == n && runHistory.at(5) == n 554 | && runHistory.at(3) == n * 3 && std::max(runHistory.at(0), runHistory.at(6)) >= n * 4; 555 | } 556 | 557 | 558 | bool QrCode::getBit(long x, int i) { 559 | return ((x >> i) & 1) != 0; 560 | } 561 | 562 | 563 | /*---- Tables of constants ----*/ 564 | 565 | const int QrCode::PENALTY_N1 = 3; 566 | const int QrCode::PENALTY_N2 = 3; 567 | const int QrCode::PENALTY_N3 = 40; 568 | const int QrCode::PENALTY_N4 = 10; 569 | 570 | 571 | const int8_t QrCode::ECC_CODEWORDS_PER_BLOCK[4][41] = { 572 | // Version: (note that index 0 is for padding, and is set to an illegal value) 573 | //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level 574 | {-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Low 575 | {-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28}, // Medium 576 | {-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Quartile 577 | {-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // High 578 | }; 579 | 580 | const int8_t QrCode::NUM_ERROR_CORRECTION_BLOCKS[4][41] = { 581 | // Version: (note that index 0 is for padding, and is set to an illegal value) 582 | //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level 583 | {-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low 584 | {-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium 585 | {-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile 586 | {-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High 587 | }; 588 | 589 | 590 | QrCode::ReedSolomonGenerator::ReedSolomonGenerator(int degree) : 591 | coefficients() { 592 | if (degree < 1 || degree > 255) 593 | throw std::domain_error("Degree out of range"); 594 | 595 | // Start with the monomial x^0 596 | coefficients.resize(degree); 597 | coefficients.at(degree - 1) = 1; 598 | 599 | // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}), 600 | // drop the highest term, and store the rest of the coefficients in order of descending powers. 601 | // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D). 602 | uint8_t root = 1; 603 | for (int i = 0; i < degree; i++) { 604 | // Multiply the current product by (x - r^i) 605 | for (size_t j = 0; j < coefficients.size(); j++) { 606 | coefficients.at(j) = multiply(coefficients.at(j), root); 607 | if (j + 1 < coefficients.size()) 608 | coefficients.at(j) ^= coefficients.at(j + 1); 609 | } 610 | root = multiply(root, 0x02); 611 | } 612 | } 613 | 614 | 615 | vector QrCode::ReedSolomonGenerator::getRemainder(const vector &data) const { 616 | // Compute the remainder by performing polynomial division 617 | vector result(coefficients.size()); 618 | for (uint8_t b : data) { 619 | uint8_t factor = b ^ result.at(0); 620 | result.erase(result.begin()); 621 | result.push_back(0); 622 | for (size_t j = 0; j < result.size(); j++) 623 | result.at(j) ^= multiply(coefficients.at(j), factor); 624 | } 625 | return result; 626 | } 627 | 628 | 629 | uint8_t QrCode::ReedSolomonGenerator::multiply(uint8_t x, uint8_t y) { 630 | // Russian peasant multiplication 631 | int z = 0; 632 | for (int i = 7; i >= 0; i--) { 633 | z = (z << 1) ^ ((z >> 7) * 0x11D); 634 | z ^= ((y >> i) & 1) * x; 635 | } 636 | if (z >> 8 != 0) 637 | throw std::logic_error("Assertion error"); 638 | return static_cast(z); 639 | } 640 | 641 | 642 | data_too_long::data_too_long(const std::string &msg) : 643 | std::length_error(msg) {} 644 | 645 | } 646 | -------------------------------------------------------------------------------- /src/include/nayuki/QrCode.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * QR Code generator library (C++) 3 | * 4 | * Copyright (c) Project Nayuki. (MIT License) 5 | * https://www.nayuki.io/page/qr-code-generator-library 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 of 11 | * the Software, and to permit persons to whom the Software is furnished to do so, 12 | * subject to the following conditions: 13 | * - The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 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 | 24 | #pragma once 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include "QrSegment.hpp" 32 | 33 | 34 | namespace qrcodegen { 35 | 36 | /* 37 | * A QR Code symbol, which is a type of two-dimension barcode. 38 | * Invented by Denso Wave and described in the ISO/IEC 18004 standard. 39 | * Instances of this class represent an immutable square grid of black and white cells. 40 | * The class provides static factory functions to create a QR Code from text or binary data. 41 | * The class covers the QR Code Model 2 specification, supporting all versions (sizes) 42 | * from 1 to 40, all 4 error correction levels, and 4 character encoding modes. 43 | * 44 | * Ways to create a QR Code object: 45 | * - High level: Take the payload data and call QrCode::encodeText() or QrCode::encodeBinary(). 46 | * - Mid level: Custom-make the list of segments and call QrCode::encodeSegments(). 47 | * - Low level: Custom-make the array of data codeword bytes (including 48 | * segment headers and final padding, excluding error correction codewords), 49 | * supply the appropriate version number, and call the QrCode() constructor. 50 | * (Note that all ways require supplying the desired error correction level.) 51 | */ 52 | class QrCode final { 53 | 54 | /*---- Public helper enumeration ----*/ 55 | 56 | /* 57 | * The error correction level in a QR Code symbol. 58 | */ 59 | public: enum class Ecc { 60 | LOW = 0 , // The QR Code can tolerate about 7% erroneous codewords 61 | MEDIUM , // The QR Code can tolerate about 15% erroneous codewords 62 | QUARTILE, // The QR Code can tolerate about 25% erroneous codewords 63 | HIGH , // The QR Code can tolerate about 30% erroneous codewords 64 | }; 65 | 66 | 67 | // Returns a value in the range 0 to 3 (unsigned 2-bit integer). 68 | private: static int getFormatBits(Ecc ecl); 69 | 70 | 71 | 72 | /*---- Static factory functions (high level) ----*/ 73 | 74 | /* 75 | * Returns a QR Code representing the given Unicode text string at the given error correction level. 76 | * As a conservative upper bound, this function is guaranteed to succeed for strings that have 2953 or fewer 77 | * UTF-8 code units (not Unicode code points) if the low error correction level is used. The smallest possible 78 | * QR Code version is automatically chosen for the output. The ECC level of the result may be higher than 79 | * the ecl argument if it can be done without increasing the version. 80 | */ 81 | public: static QrCode encodeText(const char *text, Ecc ecl); 82 | 83 | 84 | /* 85 | * Returns a QR Code representing the given binary data at the given error correction level. 86 | * This function always encodes using the binary segment mode, not any text mode. The maximum number of 87 | * bytes allowed is 2953. The smallest possible QR Code version is automatically chosen for the output. 88 | * The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version. 89 | */ 90 | public: static QrCode encodeBinary(const std::vector &data, Ecc ecl); 91 | 92 | 93 | /*---- Static factory functions (mid level) ----*/ 94 | 95 | /* 96 | * Returns a QR Code representing the given segments with the given encoding parameters. 97 | * The smallest possible QR Code version within the given range is automatically 98 | * chosen for the output. Iff boostEcl is true, then the ECC level of the result 99 | * may be higher than the ecl argument if it can be done without increasing the 100 | * version. The mask number is either between 0 to 7 (inclusive) to force that 101 | * mask, or -1 to automatically choose an appropriate mask (which may be slow). 102 | * This function allows the user to create a custom sequence of segments that switches 103 | * between modes (such as alphanumeric and byte) to encode text in less space. 104 | * This is a mid-level API; the high-level API is encodeText() and encodeBinary(). 105 | */ 106 | public: static QrCode encodeSegments(const std::vector &segs, Ecc ecl, 107 | int minVersion=1, int maxVersion=40, int mask=-1, bool boostEcl=true); // All optional parameters 108 | 109 | 110 | 111 | /*---- Instance fields ----*/ 112 | 113 | // Immutable scalar parameters: 114 | 115 | /* The version number of this QR Code, which is between 1 and 40 (inclusive). 116 | * This determines the size of this barcode. */ 117 | private: int version; 118 | 119 | /* The width and height of this QR Code, measured in modules, between 120 | * 21 and 177 (inclusive). This is equal to version * 4 + 17. */ 121 | private: int size; 122 | 123 | /* The error correction level used in this QR Code. */ 124 | private: Ecc errorCorrectionLevel; 125 | 126 | /* The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive). 127 | * Even if a QR Code is created with automatic masking requested (mask = -1), 128 | * the resulting object still has a mask value between 0 and 7. */ 129 | private: int mask; 130 | 131 | // Private grids of modules/pixels, with dimensions of size*size: 132 | 133 | // The modules of this QR Code (false = white, true = black). 134 | // Immutable after constructor finishes. Accessed through getModule(). 135 | private: std::vector > modules; 136 | 137 | // Indicates function modules that are not subjected to masking. Discarded when constructor finishes. 138 | private: std::vector > isFunction; 139 | 140 | 141 | 142 | /*---- Constructor (low level) ----*/ 143 | 144 | /* 145 | * Creates a new QR Code with the given version number, 146 | * error correction level, data codeword bytes, and mask number. 147 | * This is a low-level API that most users should not use directly. 148 | * A mid-level API is the encodeSegments() function. 149 | */ 150 | public: QrCode(int ver, Ecc ecl, const std::vector &dataCodewords, int mask); 151 | 152 | 153 | 154 | /*---- Public instance methods ----*/ 155 | 156 | /* 157 | * Returns this QR Code's version, in the range [1, 40]. 158 | */ 159 | public: int getVersion() const; 160 | 161 | 162 | /* 163 | * Returns this QR Code's size, in the range [21, 177]. 164 | */ 165 | public: int getSize() const; 166 | 167 | 168 | /* 169 | * Returns this QR Code's error correction level. 170 | */ 171 | public: Ecc getErrorCorrectionLevel() const; 172 | 173 | 174 | /* 175 | * Returns this QR Code's mask, in the range [0, 7]. 176 | */ 177 | public: int getMask() const; 178 | 179 | 180 | /* 181 | * Returns the color of the module (pixel) at the given coordinates, which is false 182 | * for white or true for black. The top left corner has the coordinates (x=0, y=0). 183 | * If the given coordinates are out of bounds, then false (white) is returned. 184 | */ 185 | public: bool getModule(int x, int y) const; 186 | 187 | 188 | /* 189 | * Returns a string of SVG code for an image depicting this QR Code, with the given number 190 | * of border modules. The string always uses Unix newlines (\n), regardless of the platform. 191 | */ 192 | public: std::string toSvgString(int border) const; 193 | 194 | 195 | 196 | /*---- Private helper methods for constructor: Drawing function modules ----*/ 197 | 198 | // Reads this object's version field, and draws and marks all function modules. 199 | private: void drawFunctionPatterns(); 200 | 201 | 202 | // Draws two copies of the format bits (with its own error correction code) 203 | // based on the given mask and this object's error correction level field. 204 | private: void drawFormatBits(int mask); 205 | 206 | 207 | // Draws two copies of the version bits (with its own error correction code), 208 | // based on this object's version field, iff 7 <= version <= 40. 209 | private: void drawVersion(); 210 | 211 | 212 | // Draws a 9*9 finder pattern including the border separator, 213 | // with the center module at (x, y). Modules can be out of bounds. 214 | private: void drawFinderPattern(int x, int y); 215 | 216 | 217 | // Draws a 5*5 alignment pattern, with the center module 218 | // at (x, y). All modules must be in bounds. 219 | private: void drawAlignmentPattern(int x, int y); 220 | 221 | 222 | // Sets the color of a module and marks it as a function module. 223 | // Only used by the constructor. Coordinates must be in bounds. 224 | private: void setFunctionModule(int x, int y, bool isBlack); 225 | 226 | 227 | // Returns the color of the module at the given coordinates, which must be in range. 228 | private: bool module(int x, int y) const; 229 | 230 | 231 | /*---- Private helper methods for constructor: Codewords and masking ----*/ 232 | 233 | // Returns a new byte string representing the given data with the appropriate error correction 234 | // codewords appended to it, based on this object's version and error correction level. 235 | private: std::vector addEccAndInterleave(const std::vector &data) const; 236 | 237 | 238 | // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire 239 | // data area of this QR Code. Function modules need to be marked off before this is called. 240 | private: void drawCodewords(const std::vector &data); 241 | 242 | 243 | // XORs the codeword modules in this QR Code with the given mask pattern. 244 | // The function modules must be marked and the codeword bits must be drawn 245 | // before masking. Due to the arithmetic of XOR, calling applyMask() with 246 | // the same mask value a second time will undo the mask. A final well-formed 247 | // QR Code needs exactly one (not zero, two, etc.) mask applied. 248 | private: void applyMask(int mask); 249 | 250 | 251 | // Calculates and returns the penalty score based on state of this QR Code's current modules. 252 | // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score. 253 | private: long getPenaltyScore() const; 254 | 255 | 256 | 257 | /*---- Private helper functions ----*/ 258 | 259 | // Returns an ascending list of positions of alignment patterns for this version number. 260 | // Each position is in the range [0,177), and are used on both the x and y axes. 261 | // This could be implemented as lookup table of 40 variable-length lists of unsigned bytes. 262 | private: std::vector getAlignmentPatternPositions() const; 263 | 264 | 265 | // Returns the number of data bits that can be stored in a QR Code of the given version number, after 266 | // all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8. 267 | // The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table. 268 | private: static int getNumRawDataModules(int ver); 269 | 270 | 271 | // Returns the number of 8-bit data (i.e. not error correction) codewords contained in any 272 | // QR Code of the given version number and error correction level, with remainder bits discarded. 273 | // This stateless pure function could be implemented as a (40*4)-cell lookup table. 274 | private: static int getNumDataCodewords(int ver, Ecc ecl); 275 | 276 | 277 | // Inserts the given value to the front of the given array, which shifts over the 278 | // existing values and deletes the last value. A helper function for getPenaltyScore(). 279 | private: static void addRunToHistory(int run, std::deque &history); 280 | 281 | 282 | // Tests whether the given run history has the pattern of ratio 1:1:3:1:1 in the middle, and 283 | // surrounded by at least 4 on either or both ends. A helper function for getPenaltyScore(). 284 | // Must only be called immediately after a run of white modules has ended. 285 | private: static bool hasFinderLikePattern(const std::deque &runHistory); 286 | 287 | 288 | // Returns true iff the i'th bit of x is set to 1. 289 | private: static bool getBit(long x, int i); 290 | 291 | 292 | /*---- Constants and tables ----*/ 293 | 294 | // The minimum version number supported in the QR Code Model 2 standard. 295 | public: static constexpr int MIN_VERSION = 1; 296 | 297 | // The maximum version number supported in the QR Code Model 2 standard. 298 | public: static constexpr int MAX_VERSION = 40; 299 | 300 | 301 | // For use in getPenaltyScore(), when evaluating which mask is best. 302 | private: static const int PENALTY_N1; 303 | private: static const int PENALTY_N2; 304 | private: static const int PENALTY_N3; 305 | private: static const int PENALTY_N4; 306 | 307 | 308 | private: static const std::int8_t ECC_CODEWORDS_PER_BLOCK[4][41]; 309 | private: static const std::int8_t NUM_ERROR_CORRECTION_BLOCKS[4][41]; 310 | 311 | 312 | 313 | /*---- Private helper class ----*/ 314 | 315 | /* 316 | * Computes the Reed-Solomon error correction codewords for a sequence of data codewords 317 | * at a given degree. Objects are immutable, and the state only depends on the degree. 318 | * This class exists because each data block in a QR Code shares the same the divisor polynomial. 319 | */ 320 | private: class ReedSolomonGenerator final { 321 | 322 | /*-- Immutable field --*/ 323 | 324 | // Coefficients of the divisor polynomial, stored from highest to lowest power, excluding the leading term which 325 | // is always 1. For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array {255, 8, 93}. 326 | private: std::vector coefficients; 327 | 328 | 329 | /*-- Constructor --*/ 330 | 331 | /* 332 | * Creates a Reed-Solomon ECC generator for the given degree. This could be implemented 333 | * as a lookup table over all possible parameter values, instead of as an algorithm. 334 | */ 335 | public: explicit ReedSolomonGenerator(int degree); 336 | 337 | 338 | /*-- Method --*/ 339 | 340 | /* 341 | * Computes and returns the Reed-Solomon error correction codewords for the given 342 | * sequence of data codewords. The returned object is always a new byte array. 343 | * This method does not alter this object's state (because it is immutable). 344 | */ 345 | public: std::vector getRemainder(const std::vector &data) const; 346 | 347 | 348 | /*-- Static function --*/ 349 | 350 | // Returns the product of the two given field elements modulo GF(2^8/0x11D). 351 | // All inputs are valid. This could be implemented as a 256*256 lookup table. 352 | private: static std::uint8_t multiply(std::uint8_t x, std::uint8_t y); 353 | 354 | }; 355 | 356 | }; 357 | 358 | 359 | 360 | /*---- Public exception class ----*/ 361 | 362 | /* 363 | * Thrown when the supplied data does not fit any QR Code version. Ways to handle this exception include: 364 | * - Decrease the error correction level if it was greater than Ecc::LOW. 365 | * - If the encodeSegments() function was called with a maxVersion argument, then increase 366 | * it if it was less than QrCode::MAX_VERSION. (This advice does not apply to the other 367 | * factory functions because they search all versions up to QrCode::MAX_VERSION.) 368 | * - Split the text data into better or optimal segments in order to reduce the number of bits required. 369 | * - Change the text or binary data to be shorter. 370 | * - Change the text to fit the character set of a particular segment mode (e.g. alphanumeric). 371 | * - Propagate the error upward to the caller/user. 372 | */ 373 | class data_too_long : public std::length_error { 374 | 375 | public: explicit data_too_long(const std::string &msg); 376 | 377 | }; 378 | 379 | } 380 | -------------------------------------------------------------------------------- /src/include/nayuki/QrSegment.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * QR Code generator library (C++) 3 | * 4 | * Copyright (c) Project Nayuki. (MIT License) 5 | * https://www.nayuki.io/page/qr-code-generator-library 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 of 11 | * the Software, and to permit persons to whom the Software is furnished to do so, 12 | * subject to the following conditions: 13 | * - The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 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 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include "QrSegment.hpp" 29 | 30 | using std::uint8_t; 31 | using std::vector; 32 | 33 | 34 | namespace qrcodegen { 35 | 36 | QrSegment::Mode::Mode(int mode, int cc0, int cc1, int cc2) : 37 | modeBits(mode) { 38 | numBitsCharCount[0] = cc0; 39 | numBitsCharCount[1] = cc1; 40 | numBitsCharCount[2] = cc2; 41 | } 42 | 43 | 44 | int QrSegment::Mode::getModeBits() const { 45 | return modeBits; 46 | } 47 | 48 | 49 | int QrSegment::Mode::numCharCountBits(int ver) const { 50 | return numBitsCharCount[(ver + 7) / 17]; 51 | } 52 | 53 | 54 | const QrSegment::Mode QrSegment::Mode::NUMERIC (0x1, 10, 12, 14); 55 | const QrSegment::Mode QrSegment::Mode::ALPHANUMERIC(0x2, 9, 11, 13); 56 | const QrSegment::Mode QrSegment::Mode::BYTE (0x4, 8, 16, 16); 57 | const QrSegment::Mode QrSegment::Mode::KANJI (0x8, 8, 10, 12); 58 | const QrSegment::Mode QrSegment::Mode::ECI (0x7, 0, 0, 0); 59 | 60 | 61 | 62 | QrSegment QrSegment::makeBytes(const vector &data) { 63 | if (data.size() > static_cast(INT_MAX)) 64 | throw std::length_error("Data too long"); 65 | BitBuffer bb; 66 | for (uint8_t b : data) 67 | bb.appendBits(b, 8); 68 | return QrSegment(Mode::BYTE, static_cast(data.size()), std::move(bb)); 69 | } 70 | 71 | 72 | QrSegment QrSegment::makeNumeric(const char *digits) { 73 | BitBuffer bb; 74 | int accumData = 0; 75 | int accumCount = 0; 76 | int charCount = 0; 77 | for (; *digits != '\0'; digits++, charCount++) { 78 | char c = *digits; 79 | if (c < '0' || c > '9') 80 | throw std::domain_error("String contains non-numeric characters"); 81 | accumData = accumData * 10 + (c - '0'); 82 | accumCount++; 83 | if (accumCount == 3) { 84 | bb.appendBits(accumData, 10); 85 | accumData = 0; 86 | accumCount = 0; 87 | } 88 | } 89 | if (accumCount > 0) // 1 or 2 digits remaining 90 | bb.appendBits(accumData, accumCount * 3 + 1); 91 | return QrSegment(Mode::NUMERIC, charCount, std::move(bb)); 92 | } 93 | 94 | 95 | QrSegment QrSegment::makeAlphanumeric(const char *text) { 96 | BitBuffer bb; 97 | int accumData = 0; 98 | int accumCount = 0; 99 | int charCount = 0; 100 | for (; *text != '\0'; text++, charCount++) { 101 | const char *temp = std::strchr(ALPHANUMERIC_CHARSET, *text); 102 | if (temp == nullptr) 103 | throw std::domain_error("String contains unencodable characters in alphanumeric mode"); 104 | accumData = accumData * 45 + (temp - ALPHANUMERIC_CHARSET); 105 | accumCount++; 106 | if (accumCount == 2) { 107 | bb.appendBits(accumData, 11); 108 | accumData = 0; 109 | accumCount = 0; 110 | } 111 | } 112 | if (accumCount > 0) // 1 character remaining 113 | bb.appendBits(accumData, 6); 114 | return QrSegment(Mode::ALPHANUMERIC, charCount, std::move(bb)); 115 | } 116 | 117 | 118 | vector QrSegment::makeSegments(const char *text) { 119 | // Select the most efficient segment encoding automatically 120 | vector result; 121 | if (*text == '\0'); // Leave result empty 122 | else if (isNumeric(text)) 123 | result.push_back(makeNumeric(text)); 124 | else if (isAlphanumeric(text)) 125 | result.push_back(makeAlphanumeric(text)); 126 | else { 127 | vector bytes; 128 | for (; *text != '\0'; text++) 129 | bytes.push_back(static_cast(*text)); 130 | result.push_back(makeBytes(bytes)); 131 | } 132 | return result; 133 | } 134 | 135 | 136 | QrSegment QrSegment::makeEci(long assignVal) { 137 | BitBuffer bb; 138 | if (assignVal < 0) 139 | throw std::domain_error("ECI assignment value out of range"); 140 | else if (assignVal < (1 << 7)) 141 | bb.appendBits(assignVal, 8); 142 | else if (assignVal < (1 << 14)) { 143 | bb.appendBits(2, 2); 144 | bb.appendBits(assignVal, 14); 145 | } else if (assignVal < 1000000L) { 146 | bb.appendBits(6, 3); 147 | bb.appendBits(assignVal, 21); 148 | } else 149 | throw std::domain_error("ECI assignment value out of range"); 150 | return QrSegment(Mode::ECI, 0, std::move(bb)); 151 | } 152 | 153 | 154 | QrSegment::QrSegment(Mode md, int numCh, const std::vector &dt) : 155 | mode(md), 156 | numChars(numCh), 157 | data(dt) { 158 | if (numCh < 0) 159 | throw std::domain_error("Invalid value"); 160 | } 161 | 162 | 163 | QrSegment::QrSegment(Mode md, int numCh, std::vector &&dt) : 164 | mode(md), 165 | numChars(numCh), 166 | data(std::move(dt)) { 167 | if (numCh < 0) 168 | throw std::domain_error("Invalid value"); 169 | } 170 | 171 | 172 | int QrSegment::getTotalBits(const vector &segs, int version) { 173 | int result = 0; 174 | for (const QrSegment &seg : segs) { 175 | int ccbits = seg.mode.numCharCountBits(version); 176 | if (seg.numChars >= (1L << ccbits)) 177 | return -1; // The segment's length doesn't fit the field's bit width 178 | if (4 + ccbits > INT_MAX - result) 179 | return -1; // The sum will overflow an int type 180 | result += 4 + ccbits; 181 | if (seg.data.size() > static_cast(INT_MAX - result)) 182 | return -1; // The sum will overflow an int type 183 | result += static_cast(seg.data.size()); 184 | } 185 | return result; 186 | } 187 | 188 | 189 | bool QrSegment::isAlphanumeric(const char *text) { 190 | for (; *text != '\0'; text++) { 191 | if (std::strchr(ALPHANUMERIC_CHARSET, *text) == nullptr) 192 | return false; 193 | } 194 | return true; 195 | } 196 | 197 | 198 | bool QrSegment::isNumeric(const char *text) { 199 | for (; *text != '\0'; text++) { 200 | char c = *text; 201 | if (c < '0' || c > '9') 202 | return false; 203 | } 204 | return true; 205 | } 206 | 207 | 208 | QrSegment::Mode QrSegment::getMode() const { 209 | return mode; 210 | } 211 | 212 | 213 | int QrSegment::getNumChars() const { 214 | return numChars; 215 | } 216 | 217 | 218 | const std::vector &QrSegment::getData() const { 219 | return data; 220 | } 221 | 222 | 223 | const char *QrSegment::ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; 224 | 225 | } 226 | -------------------------------------------------------------------------------- /src/include/nayuki/QrSegment.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * QR Code generator library (C++) 3 | * 4 | * Copyright (c) Project Nayuki. (MIT License) 5 | * https://www.nayuki.io/page/qr-code-generator-library 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 of 11 | * the Software, and to permit persons to whom the Software is furnished to do so, 12 | * subject to the following conditions: 13 | * - The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 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 | 24 | #pragma once 25 | 26 | #include 27 | #include 28 | #include "BitBuffer.hpp" 29 | 30 | 31 | namespace qrcodegen { 32 | 33 | /* 34 | * A segment of character/binary/control data in a QR Code symbol. 35 | * Instances of this class are immutable. 36 | * The mid-level way to create a segment is to take the payload data 37 | * and call a static factory function such as QrSegment::makeNumeric(). 38 | * The low-level way to create a segment is to custom-make the bit buffer 39 | * and call the QrSegment() constructor with appropriate values. 40 | * This segment class imposes no length restrictions, but QR Codes have restrictions. 41 | * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data. 42 | * Any segment longer than this is meaningless for the purpose of generating QR Codes. 43 | */ 44 | class QrSegment final { 45 | 46 | /*---- Public helper enumeration ----*/ 47 | 48 | /* 49 | * Describes how a segment's data bits are interpreted. Immutable. 50 | */ 51 | public: class Mode final { 52 | 53 | /*-- Constants --*/ 54 | 55 | public: static const Mode NUMERIC; 56 | public: static const Mode ALPHANUMERIC; 57 | public: static const Mode BYTE; 58 | public: static const Mode KANJI; 59 | public: static const Mode ECI; 60 | 61 | 62 | /*-- Fields --*/ 63 | 64 | // The mode indicator bits, which is a uint4 value (range 0 to 15). 65 | private: int modeBits; 66 | 67 | // Number of character count bits for three different version ranges. 68 | private: int numBitsCharCount[3]; 69 | 70 | 71 | /*-- Constructor --*/ 72 | 73 | private: Mode(int mode, int cc0, int cc1, int cc2); 74 | 75 | 76 | /*-- Methods --*/ 77 | 78 | /* 79 | * (Package-private) Returns the mode indicator bits, which is an unsigned 4-bit value (range 0 to 15). 80 | */ 81 | public: int getModeBits() const; 82 | 83 | /* 84 | * (Package-private) Returns the bit width of the character count field for a segment in 85 | * this mode in a QR Code at the given version number. The result is in the range [0, 16]. 86 | */ 87 | public: int numCharCountBits(int ver) const; 88 | 89 | }; 90 | 91 | 92 | 93 | /*---- Static factory functions (mid level) ----*/ 94 | 95 | /* 96 | * Returns a segment representing the given binary data encoded in 97 | * byte mode. All input byte vectors are acceptable. Any text string 98 | * can be converted to UTF-8 bytes and encoded as a byte mode segment. 99 | */ 100 | public: static QrSegment makeBytes(const std::vector &data); 101 | 102 | 103 | /* 104 | * Returns a segment representing the given string of decimal digits encoded in numeric mode. 105 | */ 106 | public: static QrSegment makeNumeric(const char *digits); 107 | 108 | 109 | /* 110 | * Returns a segment representing the given text string encoded in alphanumeric mode. 111 | * The characters allowed are: 0 to 9, A to Z (uppercase only), space, 112 | * dollar, percent, asterisk, plus, hyphen, period, slash, colon. 113 | */ 114 | public: static QrSegment makeAlphanumeric(const char *text); 115 | 116 | 117 | /* 118 | * Returns a list of zero or more segments to represent the given text string. The result 119 | * may use various segment modes and switch modes to optimize the length of the bit stream. 120 | */ 121 | public: static std::vector makeSegments(const char *text); 122 | 123 | 124 | /* 125 | * Returns a segment representing an Extended Channel Interpretation 126 | * (ECI) designator with the given assignment value. 127 | */ 128 | public: static QrSegment makeEci(long assignVal); 129 | 130 | 131 | /*---- Public static helper functions ----*/ 132 | 133 | /* 134 | * Tests whether the given string can be encoded as a segment in alphanumeric mode. 135 | * A string is encodable iff each character is in the following set: 0 to 9, A to Z 136 | * (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon. 137 | */ 138 | public: static bool isAlphanumeric(const char *text); 139 | 140 | 141 | /* 142 | * Tests whether the given string can be encoded as a segment in numeric mode. 143 | * A string is encodable iff each character is in the range 0 to 9. 144 | */ 145 | public: static bool isNumeric(const char *text); 146 | 147 | 148 | 149 | /*---- Instance fields ----*/ 150 | 151 | /* The mode indicator of this segment. Accessed through getMode(). */ 152 | private: Mode mode; 153 | 154 | /* The length of this segment's unencoded data. Measured in characters for 155 | * numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode. 156 | * Always zero or positive. Not the same as the data's bit length. 157 | * Accessed through getNumChars(). */ 158 | private: int numChars; 159 | 160 | /* The data bits of this segment. Accessed through getData(). */ 161 | private: std::vector data; 162 | 163 | 164 | /*---- Constructors (low level) ----*/ 165 | 166 | /* 167 | * Creates a new QR Code segment with the given attributes and data. 168 | * The character count (numCh) must agree with the mode and the bit buffer length, 169 | * but the constraint isn't checked. The given bit buffer is copied and stored. 170 | */ 171 | public: QrSegment(Mode md, int numCh, const std::vector &dt); 172 | 173 | 174 | /* 175 | * Creates a new QR Code segment with the given parameters and data. 176 | * The character count (numCh) must agree with the mode and the bit buffer length, 177 | * but the constraint isn't checked. The given bit buffer is moved and stored. 178 | */ 179 | public: QrSegment(Mode md, int numCh, std::vector &&dt); 180 | 181 | 182 | /*---- Methods ----*/ 183 | 184 | /* 185 | * Returns the mode field of this segment. 186 | */ 187 | public: Mode getMode() const; 188 | 189 | 190 | /* 191 | * Returns the character count field of this segment. 192 | */ 193 | public: int getNumChars() const; 194 | 195 | 196 | /* 197 | * Returns the data bits of this segment. 198 | */ 199 | public: const std::vector &getData() const; 200 | 201 | 202 | // (Package-private) Calculates the number of bits needed to encode the given segments at 203 | // the given version. Returns a non-negative number if successful. Otherwise returns -1 if a 204 | // segment has too many characters to fit its length field, or the total bits exceeds INT_MAX. 205 | public: static int getTotalBits(const std::vector &segs, int version); 206 | 207 | 208 | /*---- Private constant ----*/ 209 | 210 | /* The set of all legal characters in alphanumeric mode, where 211 | * each character value maps to the index in the string. */ 212 | private: static const char *ALPHANUMERIC_CHARSET; 213 | 214 | }; 215 | 216 | } 217 | -------------------------------------------------------------------------------- /src/include/pam_oauth2_curl.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jens on 23/07/2021. 3 | // 4 | 5 | #include "pam_oauth2_curl.hpp" 6 | #include "pam_oauth2_curl_impl.hpp" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "config.hpp" 12 | #include "pam_oauth2_excpt.hpp" 13 | 14 | 15 | /* 16 | #if LIBCURL_VERSION_MAJOR < 7 || LIBCURL_VERSION_MINOR < 60 17 | #error "Must have at least curl 7.60.0" 18 | #endif 19 | */ 20 | 21 | pam_oauth2_curl::pam_oauth2_curl(Config const &config): impl_(new pam_oauth2_curl_impl(config)) 22 | { 23 | if(!impl_) 24 | throw NetworkError("curl: Failed to create impl (out of memory?)"); 25 | // shared options for all calls 26 | } 27 | 28 | 29 | pam_oauth2_curl::~pam_oauth2_curl() 30 | { 31 | delete impl_; 32 | } 33 | 34 | 35 | pam_oauth2_curl::credential 36 | pam_oauth2_curl::make_credential(Config const &config) 37 | { 38 | // CHTC patch (revised again). Add secret (see RFC 6749 section 2.3.1) to parameters 39 | if(!config.http_basic_auth) 40 | { 41 | return std::move(pam_oauth2_curl::credential(config.client_id, config.client_secret, 0)); 42 | } 43 | return std::move(credential(config.client_id, config.client_secret)); 44 | } 45 | 46 | 47 | 48 | // TODO still too much code duplication between the calls 49 | 50 | std::string 51 | pam_oauth2_curl::call(Config const &config, std::string const &url) 52 | { 53 | call_data readBuffer; 54 | impl_->reset(config); 55 | impl_->add_call_data(readBuffer); 56 | impl_->add_credential(readBuffer, make_credential(config)); 57 | curl_easy_setopt(impl_->curl, CURLOPT_URL, url.c_str()); 58 | curl_easy_setopt(impl_->curl, CURLOPT_ERRORBUFFER, readBuffer.errbuf); 59 | if(config.client_debug) { 60 | curl_easy_setopt(impl_->curl, CURLOPT_VERBOSE, 1); 61 | } 62 | 63 | CURLcode res = curl_easy_perform(impl_->curl); 64 | 65 | if(res != CURLE_OK) { 66 | NetworkError err("curl failed HTTP call"); 67 | err.add_details(readBuffer.errbuf); 68 | throw err; 69 | } 70 | return readBuffer.callback_data; 71 | } 72 | 73 | 74 | std::string 75 | pam_oauth2_curl::call(Config const &config, const std::string &url, 76 | std::vector> const &postdata) 77 | { 78 | call_data readBuffer; 79 | std::string params{pam_oauth2_curl_impl::make_post_data(postdata)}; 80 | impl_->reset(config); 81 | impl_->add_call_data(readBuffer); 82 | impl_->add_credential(readBuffer, make_credential(config)); 83 | curl_easy_setopt(impl_->curl, CURLOPT_URL, url.c_str()); 84 | curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDS, params.c_str()); 85 | curl_easy_setopt(impl_->curl, CURLOPT_ERRORBUFFER, readBuffer.errbuf); 86 | if(config.client_debug) { 87 | curl_easy_setopt(impl_->curl, CURLOPT_VERBOSE, 1); 88 | } 89 | // Automatically POSTs because we set postfields 90 | CURLcode res = curl_easy_perform(impl_->curl); 91 | if(res != CURLE_OK) { 92 | NetworkError err("curl failed HTTP POST"); 93 | err.add_details(readBuffer.errbuf); 94 | throw err; 95 | } 96 | return readBuffer.callback_data; 97 | } 98 | 99 | 100 | std::string 101 | pam_oauth2_curl::call(Config const &config, const std::string &url, credential &&cred) 102 | { 103 | call_data readBuffer; 104 | impl_->reset(config); 105 | impl_->add_call_data(readBuffer); 106 | impl_->add_credential(readBuffer, std::move(cred)); 107 | curl_easy_setopt(impl_->curl, CURLOPT_URL, url.c_str()); 108 | curl_easy_setopt(impl_->curl, CURLOPT_ERRORBUFFER, readBuffer.errbuf); 109 | if(config.client_debug) { 110 | curl_easy_setopt(impl_->curl, CURLOPT_VERBOSE, 1); 111 | } 112 | CURLcode res = curl_easy_perform(impl_->curl); 113 | if(res != CURLE_OK) { 114 | NetworkError err("curl failed HTTP GET"); 115 | err.add_details(readBuffer.errbuf); 116 | throw err; 117 | } 118 | return readBuffer.callback_data; 119 | } 120 | 121 | 122 | 123 | pam_oauth2_curl::params & 124 | pam_oauth2_curl::add_params(pam_oauth2_curl::params ¶ms, std::string const &key, std::string const &value) 125 | { 126 | return impl_->add_params(params, key, value); 127 | } 128 | 129 | 130 | std::string 131 | pam_oauth2_curl::encode(std::string const &in) 132 | { 133 | return impl_->encode(in); 134 | } 135 | 136 | 137 | pam_oauth2_curl::credential::~credential() 138 | { 139 | if(!pw_.empty()) 140 | for( auto &c : pw_ ) 141 | c = '*'; 142 | if(!token_.empty()) 143 | for( auto &c : token_ ) 144 | c = '*'; 145 | } 146 | 147 | 148 | 149 | // pam_oauth2_curl_impl implementation 150 | 151 | pam_oauth2_curl_impl::pam_oauth2_curl_impl(Config const &config): curl{nullptr}, ret(CURLE_OK) 152 | { 153 | curl = curl_easy_init(); 154 | if(!curl) 155 | throw NetworkError("curl: cannot initialise curl"); 156 | reset(config); 157 | } 158 | 159 | 160 | pam_oauth2_curl_impl::~pam_oauth2_curl_impl() 161 | { 162 | if(curl) { 163 | curl_easy_cleanup(curl); 164 | curl = nullptr; 165 | } 166 | } 167 | 168 | 169 | 170 | 171 | void 172 | pam_oauth2_curl_impl::reset(const Config &config) 173 | { 174 | // reset to shared options 175 | curl_easy_reset(curl); 176 | if(curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L) != CURLE_OK) 177 | throw NetworkError("curl setup cannot set verifypeer"); 178 | // Note 2 below 179 | if(curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L) != CURLE_OK) 180 | throw NetworkError("curl setup cannot set verifyhost"); 181 | 182 | // Prefer the bundle over the path for NSS compat 183 | if(!config.tls_ca_bundle.empty()) { 184 | if(curl_easy_setopt(curl, CURLOPT_CAINFO, config.tls_ca_bundle.c_str()) != CURLE_OK) 185 | throw NetworkError("curl setup cannot set CA bundle"); 186 | } 187 | else if(!config.tls_ca_path.empty()) { 188 | if(curl_easy_setopt(curl, CURLOPT_CAPATH, config.tls_ca_path.c_str()) != CURLE_OK) 189 | throw NetworkError("curl setup cannot set CA path"); 190 | } else { 191 | // FIXME warning? or error? 192 | } 193 | //(void)curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, buf); 194 | } 195 | 196 | 197 | void 198 | pam_oauth2_curl_impl::add_call_data(call_data &data) 199 | { 200 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); 201 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data.callback_data); 202 | } 203 | 204 | 205 | void 206 | pam_oauth2_curl_impl::add_credential(call_data &data, pam_oauth2_curl::credential &&cred) 207 | { 208 | // Even if the compiler defies us and copies cred, we MUST work with _our_ copy of cred 209 | // so the lifetime of the strings is no shorter than that of the call 210 | data.cred = std::move(cred); 211 | switch(data.cred.type_) 212 | { 213 | case pam_oauth2_curl::credential::type::NONE: 214 | break; 215 | case pam_oauth2_curl::credential::type::BASIC: 216 | // curl will encode for us if necessary? 217 | curl_easy_setopt(curl, CURLOPT_USERNAME, data.cred.un_.c_str()); 218 | curl_easy_setopt(curl, CURLOPT_PASSWORD, data.cred.pw_.c_str()); 219 | break; 220 | case pam_oauth2_curl::credential::type::TOKEN: 221 | data.auz_hdr = "Authorization: Bearer "; 222 | // FIXME do we need to encode the token ourselves? 223 | data.auz_hdr += encode(data.cred.token_); 224 | data.headers = curl_slist_append(data.headers, data.auz_hdr.c_str()); 225 | if(!data.headers) 226 | throw std::bad_alloc(); 227 | // FIXME if going through a proxy, the proxy server would also get the headers... 228 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, data.headers); 229 | break; 230 | case pam_oauth2_curl::credential::type::SECRET: 231 | // See RFC6749 section 2.3.1. 232 | // TODO: check if the connection is secure? 233 | add_params(data.post_data, "client_id", encode(cred.un_)); 234 | add_params(data.post_data, "client_secret", encode(cred.pw_)); 235 | break; 236 | case pam_oauth2_curl::credential::type::DENIED: 237 | throw NetworkError("default denied is not overridden"); 238 | default: 239 | throw "Cannot happen XIQJA"; 240 | } 241 | } 242 | 243 | 244 | 245 | // RFC 3986 reserved characters to be % encoded 246 | std::string pam_oauth2_curl_impl::reserved = ":/?#[]@!$&'()*+,;=%"; 247 | 248 | 249 | // Annoyingly this can no longer be static because it needs a curl handle. 250 | // Nor constexpr (if strings were constexpr) 251 | std::string 252 | pam_oauth2_curl_impl::encode(std::string const &in) 253 | { 254 | char *encode = curl_easy_escape(curl, in.c_str(), in.size()); 255 | if(!encode) throw std::bad_alloc(); 256 | std::string result(encode); 257 | curl_free(encode); 258 | return result; 259 | } 260 | 261 | 262 | bool 263 | pam_oauth2_curl_impl::contains_reserved(const std::string &str) 264 | { 265 | return str.find_first_of(reserved) != std::string::npos; 266 | } 267 | 268 | 269 | 270 | std::string 271 | pam_oauth2_curl_impl::make_post_data(pam_oauth2_curl::params const &data) 272 | { 273 | std::string tmp; 274 | for( auto const &p : data ) { 275 | if(!tmp.empty()) 276 | tmp.append("&"); 277 | tmp.append(p.first); 278 | tmp.append("="); 279 | tmp.append(p.second); 280 | } 281 | return tmp; 282 | } 283 | 284 | 285 | pam_oauth2_curl::params & 286 | pam_oauth2_curl_impl::add_params(pam_oauth2_curl::params ¶ms, std::string const &key, std::string const &value) 287 | { 288 | if(pam_oauth2_curl_impl::contains_reserved(key)) 289 | throw "Cannot happen QPAKD"; 290 | // Check if it is already in there using some very lispy code 291 | auto q = params.end(); 292 | pam_oauth2_curl::params::iterator p = std::find_if(params.begin(), q, 293 | // The lambda can't use auto in C++11... 294 | [&key](std::pair const &pair) { return key == pair.first; }); 295 | std::string key_copy(key); 296 | if(p == q) 297 | { 298 | params.push_back(std::make_pair(std::move(key_copy), encode(value))); 299 | } else { 300 | // TODO warn if we are changing the value? 301 | p->second = value; 302 | } 303 | return params; 304 | } 305 | 306 | 307 | 308 | size_t 309 | WriteCallback(char const *contents, size_t size, size_t nmemb, void *userp) 310 | { 311 | ((call_data *)userp)->callback_data.append((char const *)contents, size * nmemb); 312 | return size * nmemb; 313 | } 314 | -------------------------------------------------------------------------------- /src/include/pam_oauth2_curl.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jens on 23/07/2021. 3 | // PIMPL/RAII abstraction of the curl library for pam_oauth2_device 4 | // for synchronous HTTPS GET and POST as required by pam_oauth2_device 5 | // 6 | // NOTES: 7 | // 1. Functions can throw exceptions which are currently defined in pam_oauth2_device.hpp 8 | // 2. Not thread safe since curl_global_init is not thread safe, and the calls use the same curl handle throughout 9 | // 10 | 11 | #ifndef __PAM_OAUTH2_DEVICE_PAM_OAUTH2_CURL_HPP 12 | #define __PAM_OAUTH2_DEVICE_PAM_OAUTH2_CURL_HPP 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | 20 | // pimpl (defined in pam_oauth2_curl_impl.hpp and implemented in pam_oauth2_curl.cpp) 21 | class pam_oauth2_curl_impl; 22 | // declared in pam_oauth2_log.hpp 23 | // class pam_oauth2_log; 24 | // defined in config.hpp 25 | class Config; 26 | 27 | 28 | 29 | class pam_oauth2_curl { 30 | private: 31 | // A unique pointer needs to know the size of the pointee which goes against the logic of the pimpl 32 | pam_oauth2_curl_impl *impl_; 33 | // pam_oauth2_log &log_; 34 | public: 35 | pam_oauth2_curl(Config const &config); 36 | // pam_oauth2_curl(Config const &config, pam_oauth2_log &logger); 37 | ~pam_oauth2_curl(); 38 | pam_oauth2_curl(pam_oauth2_curl const &) = delete; 39 | pam_oauth2_curl(pam_oauth2_curl &&) = delete; 40 | pam_oauth2_curl &operator=(pam_oauth2_curl const &) = delete; 41 | pam_oauth2_curl &operator=(pam_oauth2_curl &&) = delete; 42 | 43 | //! parameter list; use add_params to add stuff to it (caller should treat it as an opaque type) 44 | using params = std::vector>; 45 | 46 | // Need separate credentials to accommodate CHTC patches. 47 | // The reference is RFC 6749 section 2.3.1 (and RFC2617) 48 | // This would have been a std::variant perhaps in later versions of C++ 49 | class credential { 50 | protected: 51 | /** @brief Different types of credentials 52 | - DENIED is always a failed authentication, can be used as default 53 | - NONE is no authentication, server has to reject if it doesn't like it 54 | - BASIC is RFC2617 username and password, OAuth2 style 55 | - TOKEN is bearer token 56 | - SECRET is sending the client_id/secret (as NOT RECOMMENDED by RFC6749 section 2.3.1...) 57 | */ 58 | enum class type { DENIED, NONE, BASIC, TOKEN, SECRET } type_; 59 | std::string un_, pw_; 60 | std::string token_; 61 | public: 62 | credential() : type_(credential::type::NONE), un_(), pw_(), token_() { } 63 | credential(std::string const &username, std::string const &password) : type_(credential::type::BASIC), un_(username), pw_(password), token_() { } 64 | credential(std::string const &token) : type_(credential::type::TOKEN), un_(), pw_(), token_(token) { } 65 | // The int is just there to disambiguate the overload, in lieu of something cleverer like a factory 66 | credential(std::string const &client_id, std::string const &client_secret, int) : type_(credential::type::SECRET), un_(client_id), pw_(client_secret), token_() { } 67 | // don't copy if possible 68 | credential(credential const &) = delete; 69 | credential &operator=(credential const &) = delete; 70 | // move only 71 | credential(credential &&) = default; 72 | credential &operator=(credential &&) = default; 73 | ~credential(); 74 | friend class pam_oauth2_curl_impl; 75 | }; 76 | 77 | //! @brief Credential factory kind of thing 78 | credential make_credential(Config const &); 79 | 80 | //! perform a HTTP GET or POST synchronously, returning result 81 | std::string call(Config const &config, std::string const &url); 82 | //! perform a HTTP POST synchronously, returning result 83 | std::string call(Config const &config, std::string const &url, params const &postdata); 84 | //! perform a HTTP call with custom credentials 85 | std::string call(Config const &config, std::string const &url, credential &&cred); 86 | //! add parameters to parameter list 87 | params &add_params(params ¶ms, std::string const &key, std::string const &value); 88 | //! URL encode. 89 | std::string encode(std::string const &); 90 | }; 91 | 92 | 93 | #endif //__PAM_OAUTH2_DEVICE_PAM_OAUTH2_CURL_HPP 94 | -------------------------------------------------------------------------------- /src/include/pam_oauth2_curl_impl.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jens on 26/07/2021. 3 | // This is a separate header file to make it easier to regression test the implementation 4 | // 5 | 6 | #ifndef __PAM_OAUTH2_DEVICE_PAM_OAUTH2_CURL_IMPL_HPP 7 | #define __PAM_OAUTH2_DEVICE_PAM_OAUTH2_CURL_IMPL_HPP 8 | 9 | #include "pam_oauth2_curl.hpp" 10 | #include 11 | 12 | 13 | // namespace pam_oauth2_curl_impl { 14 | 15 | //@brief callback for curl 16 | size_t WriteCallback(char const *contents, size_t size, size_t nmemb, void *userp); 17 | 18 | 19 | //! @brief small structure for handling aux metadata (not stored in CURL handle) for each POST or GET call 20 | struct call_data { 21 | std::string callback_data; 22 | std::string auz_hdr; 23 | pam_oauth2_curl::params post_data; 24 | curl_slist *headers; 25 | pam_oauth2_curl::credential cred; 26 | // Classic C string error buffer 27 | char errbuf[CURL_ERROR_SIZE]; 28 | 29 | call_data() : callback_data(), auz_hdr(), post_data(), headers(nullptr), cred() { } 30 | ~call_data() 31 | { 32 | if(headers) curl_slist_free_all(headers); 33 | } 34 | }; 35 | 36 | 37 | 38 | struct pam_oauth2_curl_impl { 39 | CURL *curl; 40 | CURLcode ret; 41 | std::vector calls; 42 | static std::string make_post_data(pam_oauth2_curl::params const &data); 43 | // RFC 3986 section 2.2 (and 2.4 for '%'). 44 | static std::string reserved; 45 | // from curl.h; the enum id of the SSL backend that we are using 46 | // curlssl_backend backend; 47 | 48 | public: 49 | pam_oauth2_curl_impl(Config const &config); 50 | ~pam_oauth2_curl_impl(); 51 | //@ reset curl handle to only have basic shared options 52 | void reset(Config const &config); 53 | //@ add callback data, linking the curl handle to a particular call_data (which are specific to each call) 54 | void add_call_data(call_data &); 55 | //@ Add a credential to the current curl options/header 56 | void add_credential(call_data &data, pam_oauth2_curl::credential &&cred); 57 | //@ RFC3986 encode 58 | std::string encode(std::string const &in); 59 | //@ Add parameters to a parameter list. 60 | // This needs to be here so we can call it from add_credential because it is no longer static 61 | pam_oauth2_curl::params &add_params(pam_oauth2_curl::params ¶ms, std::string const &key, std::string const &value); 62 | //@ return true if string contains a reserved character 63 | static bool contains_reserved(std::string const &); 64 | }; 65 | 66 | 67 | //} // namespace pam_oauth2_curl_impl 68 | 69 | #endif //__PAM_OAUTH2_DEVICE_PAM_OAUTH2_CURL_IMPL_HPP 70 | -------------------------------------------------------------------------------- /src/include/pam_oauth2_excpt.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jens on 27/07/2021. 3 | // 4 | // Exceptions and logging - definitions 5 | 6 | #ifndef __PAM_OAUTH2_DEVICE_PAM_OAUTH2_EXCPT_HPP 7 | #define __PAM_OAUTH2_DEVICE_PAM_OAUTH2_EXCPT_HPP 8 | 9 | 10 | #include 11 | #include 12 | // How portable is this? 13 | #include 14 | #include "pam_oauth2_log.hpp" 15 | 16 | 17 | class BaseError : public std::exception 18 | { 19 | // TODO temporary solution? 20 | std::string msg_, details_; 21 | // Severity level to log this exception at 22 | pam_oauth2_log::log_level_t severity_; 23 | // The logger is our friend 24 | friend class pam_oauth2_log; 25 | public: 26 | BaseError(char const *msg, pam_oauth2_log::log_level_t severity = pam_oauth2_log::log_level_t::ERR) : msg_(msg), details_(), severity_(severity) { } 27 | 28 | char const *what() const noexcept override { return msg_.c_str(); } 29 | 30 | // Disable copy 31 | BaseError(BaseError const &) = delete; 32 | BaseError &operator=(BaseError const &) = delete; 33 | // Allow moves 34 | BaseError(BaseError &&) = default; 35 | BaseError &operator=(BaseError &&) = default; 36 | 37 | //! Optionally add details (like debug info) for longer messages 38 | void add_details(std::string &&details) { details_ = std::move(details); } 39 | //! Return the details 40 | std::string const &details() const noexcept { return details_; } 41 | 42 | //! Return a four character string with the name (or near enough) of the class 43 | virtual char const *type() const noexcept { return "BASE"; } 44 | //! If this gives rise to a PAM error, what is it? 45 | virtual int pam_error() const noexcept { return PAM_AUTH_ERR; } 46 | }; 47 | 48 | 49 | struct ConfigError : public BaseError 50 | { 51 | ConfigError(char const *msg, pam_oauth2_log::log_level_t severity = pam_oauth2_log::log_level_t::ERR) : BaseError(msg, severity) { } 52 | char const *type() const noexcept override { return "CONF"; } 53 | }; 54 | 55 | 56 | struct PamError : public BaseError 57 | { 58 | PamError(char const *msg, pam_oauth2_log::log_level_t severity = pam_oauth2_log::log_level_t::ERR) : BaseError(msg, severity) { } 59 | char const *type() const noexcept override { return "PAM "; } 60 | int pam_error() const noexcept override { return PAM_SYSTEM_ERR; } 61 | }; 62 | 63 | struct NetworkError : public BaseError 64 | { 65 | NetworkError(char const *msg, pam_oauth2_log::log_level_t severity = pam_oauth2_log::log_level_t::ERR) : BaseError(msg, severity) { } 66 | char const *type() const noexcept override { return "NETW"; } 67 | }; 68 | 69 | struct TimeoutError : public NetworkError 70 | { 71 | TimeoutError(char const *msg, pam_oauth2_log::log_level_t severity = pam_oauth2_log::log_level_t::ERR) : NetworkError(msg, severity) { } 72 | char const *type() const noexcept override { return "TIME"; } 73 | }; 74 | 75 | struct ResponseError : public NetworkError 76 | { 77 | ResponseError(char const *msg, pam_oauth2_log::log_level_t severity = pam_oauth2_log::log_level_t::ERR) : NetworkError(msg, severity) { } 78 | char const *type() const noexcept override { return "RESP"; } 79 | }; 80 | 81 | 82 | #endif //__PAM_OAUTH2_DEVICE_PAM_OAUTH2_EXCPT_HPP 83 | -------------------------------------------------------------------------------- /src/include/pam_oauth2_log.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jens on 18/08/2021. 3 | // 4 | 5 | #include "pam_oauth2_log.hpp" 6 | #include "pam_oauth2_excpt.hpp" 7 | #include 8 | #include 9 | 10 | 11 | 12 | pam_oauth2_log::pam_oauth2_log(pam_handle *ph, log_level_t lev) noexcept : ph_(ph), lev_(lev), log_(nullptr) 13 | { 14 | if(lev == log_level_t::DEBUG || !ph) 15 | // TODO needs more thought 16 | log_ = stderr; 17 | } 18 | 19 | 20 | pam_oauth2_log::~pam_oauth2_log() 21 | { 22 | if(log_) { 23 | fclose(log_); 24 | log_ = nullptr; 25 | } 26 | } 27 | 28 | 29 | 30 | //constexpr 31 | bool 32 | pam_oauth2_log::log_this(log_level_t severity) const noexcept 33 | { 34 | // The disadvantage of closed class enums? This would be easier in later standards 35 | switch(lev_) 36 | { 37 | case log_level_t::DEBUG: 38 | if(severity == log_level_t::DEBUG) 39 | return true; 40 | case log_level_t::INFO: 41 | if(severity == log_level_t::INFO) 42 | return true; 43 | case log_level_t::WARN: 44 | if(severity == log_level_t::WARN) 45 | return true; 46 | case log_level_t::ERR: 47 | if(severity == log_level_t::ERR) 48 | return true; 49 | case log_level_t::OFF: 50 | return false; 51 | } 52 | return false; 53 | } 54 | 55 | 56 | //constexpr 57 | int 58 | pam_oauth2_log::syslog_pri(log_level_t level) const noexcept 59 | { 60 | // Facility for pam modules 61 | int pri = LOG_AUTHPRIV; 62 | switch(level) 63 | { 64 | case log_level_t::DEBUG: 65 | pri |= LOG_DEBUG; 66 | break; 67 | case log_level_t::INFO: 68 | pri |= LOG_INFO; 69 | break; 70 | case log_level_t::WARN: 71 | pri |= LOG_WARNING; 72 | break; 73 | case log_level_t::ERR: 74 | pri |= LOG_ERR; 75 | break; 76 | case log_level_t::OFF: 77 | break; //can't happen 78 | } 79 | return pri; 80 | } 81 | 82 | 83 | 84 | void 85 | pam_oauth2_log::log(BaseError const &e) noexcept 86 | { 87 | if(lev_ == log_level_t::OFF) 88 | return; 89 | // Simple log 90 | if(ph_) { 91 | pam_syslog(ph_, syslog_pri(e.severity_), "%s", e.what()); 92 | if(!e.details().empty()) 93 | pam_syslog(ph_, syslog_pri(e.severity_), "** %s", e.details().c_str()); 94 | } 95 | if(log_) 96 | { 97 | // short message 98 | fprintf(log_, "[%4s] %s\n", e.type(), e.what()); 99 | if(!e.details().empty()) 100 | { 101 | // todo? make this better formatted 102 | fprintf(log_, "%s\n", e.details().c_str()); 103 | } 104 | } 105 | } 106 | 107 | 108 | void 109 | pam_oauth2_log::log(log_level_t level, const char *fmt, ...) noexcept 110 | { 111 | if(lev_ == log_level_t::OFF) 112 | return; 113 | va_list ap1, ap2; 114 | va_start(ap1, fmt); 115 | va_copy(ap2, ap1); // dest, src 116 | if(ph_) 117 | pam_vsyslog(ph_, syslog_pri(level), fmt, ap1); 118 | if(log_) 119 | vfprintf(log_, fmt, ap2); 120 | va_end(ap1); 121 | va_end(ap2); 122 | } 123 | 124 | 125 | void 126 | pam_oauth2_log::log(std::exception const &e) noexcept 127 | { 128 | if(lev_ == log_level_t::OFF) 129 | return; 130 | if(ph_) 131 | pam_syslog(ph_, LOG_ERR, "system excpt %s", e.what()); 132 | if(log_) 133 | fprintf(log_, "system exception %s\n", e.what()); 134 | } 135 | -------------------------------------------------------------------------------- /src/include/pam_oauth2_log.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jens on 18/08/2021. 3 | // 4 | 5 | #ifndef __PAM_OAUTH2_DEVICE_PAM_OAUTH2_LOG_HPP 6 | #define __PAM_OAUTH2_DEVICE_PAM_OAUTH2_LOG_HPP 7 | 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | struct pam_handle; 14 | class BaseError; // defined in pam_oauth2_excpt 15 | 16 | 17 | class pam_oauth2_log { 18 | public: 19 | //! log levels a subset of those of syslog. 20 | // Note errors cannot be masked. 21 | enum class log_level_t { DEBUG, INFO, WARN, ERR, OFF }; 22 | 23 | //! simple compare against the class' log level 24 | // constexpr limitations in C++11? 25 | bool log_this(log_level_t severity) const noexcept; 26 | 27 | //! Constructor for logger 28 | //! Will accept a null pointer for the PAM handle in which case it logs to stderr (useful for testing, perhaps) 29 | //! With a PAM handle it logs to syslog via PAM 30 | pam_oauth2_log(pam_handle *ph, log_level_t lev) noexcept; 31 | // no copy, but move is OK 32 | pam_oauth2_log(pam_oauth2_log const &) = delete; 33 | pam_oauth2_log(pam_oauth2_log &&) = default; 34 | pam_oauth2_log &operator=(pam_oauth2_log const &) = delete; 35 | pam_oauth2_log &operator=(pam_oauth2_log &&) = default; 36 | ~pam_oauth2_log(); 37 | 38 | //! Query the log level 39 | log_level_t log_level() const noexcept { return lev_; } 40 | //! Change the log level 41 | // This can't be constexpr in C++11 42 | void set_log_level(log_level_t logLevel) noexcept { lev_ = logLevel; } 43 | //! log an exception 44 | void log(std::exception const &) noexcept; 45 | //! log one of our exceptions... 46 | void log(BaseError const &) noexcept; 47 | //! log a string at a specific level 48 | //! C stdarg style (not a C++ pack) 49 | void log(log_level_t, char const *fmt, ...) noexcept; 50 | //! Return the pam handle 51 | pam_handle const *get_pam_handle() { return ph_; } 52 | 53 | private: 54 | //! Translation to syslog priority 55 | // constexpr limitations in C++11? 56 | int syslog_pri(log_level_t) const noexcept; 57 | 58 | // The PAM handle 59 | pam_handle *ph_; 60 | // The current log level, messages at lower level are not reported 61 | log_level_t lev_; 62 | // File for non-syslog output, or null 63 | FILE *log_; 64 | }; 65 | 66 | 67 | 68 | 69 | #endif //__PAM_OAUTH2_DEVICE_PAM_OAUTH2_LOG_HPP 70 | -------------------------------------------------------------------------------- /src/pam_oauth2_device.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PAM_OAUTH2_DEVICE_HPP 2 | #define PAM_OAUTH2_DEVICE_HPP 3 | 4 | #include 5 | #include 6 | #include "include/pam_oauth2_log.hpp" 7 | 8 | 9 | /*! @brief userinfo type object (cf RFC 7662) 10 | */ 11 | 12 | class Userinfo 13 | { 14 | private: 15 | std::string sub_, 16 | username_, 17 | name_; 18 | // groups will be sorted alphabetically 19 | std::vector groups_; 20 | public: 21 | Userinfo(std::string const &sub, std::string const &username, std::string const &name): sub_(sub), username_(username), name_(name) {} 22 | 23 | /*! @brief Add a group to the userinfo groups. 24 | * Caution: there is no check whether the group is already in the userinfo groups. 25 | */ 26 | void add_group(std::string const &group); 27 | 28 | /*! @brief Import a vector of group names into the userinfo groups. 29 | * If there are already groups set, they will be removed (no merge). 30 | */ 31 | void set_groups(std::vector const &groups); 32 | 33 | std::string name() const { return name_; } 34 | std::string sub() const { return sub_; } 35 | std::string username() const { return username_; } 36 | 37 | //! Check if a given group is part of the userinfo groups 38 | bool is_member(std::string const &group) const; 39 | 40 | //! @brief Check whether groups (must be _sorted_; specifed through iterators) have any overlap with the userinfo groups. 41 | //! False is returned only if they are wholly distinct. 42 | bool intersects(std::vector::const_iterator beg, 43 | std::vector::const_iterator end) const; 44 | }; 45 | 46 | 47 | // TODO: improve this struct 48 | class DeviceAuthResponse 49 | { 50 | public: 51 | std::string user_code, 52 | verification_uri, 53 | verification_uri_complete, 54 | device_code; 55 | std::string get_prompt(const int qr_ecc); 56 | }; 57 | 58 | void make_authorization_request(Config const &config, 59 | pam_oauth2_log &logger, 60 | std::string const &client_id, 61 | std::string const &client_secret, 62 | std::string const &scope, 63 | std::string const &device_endpoint, 64 | DeviceAuthResponse *response); 65 | 66 | void poll_for_token(Config const &config, 67 | pam_oauth2_log &logger, 68 | std::string const &client_id, 69 | std::string const &client_secret, 70 | std::string const &token_endpoint, 71 | std::string const &device_code, 72 | std::string &token); 73 | 74 | Userinfo get_userinfo(Config const &config, 75 | pam_oauth2_log &logger, 76 | std::string const &userinfo_endpoint, 77 | std::string const &token, 78 | std::string const &username_attribute); 79 | 80 | #endif // PAM_OAUTH2_DEVICE_HPP 81 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | # Assume Google Test is installed on the system 2 | # GTEST_DIR = ./googletest-release-1.8.1/googletest 3 | GTEST_DIR = /usr 4 | 5 | SRC_DIR = ../src 6 | 7 | CPPFLAGS += -isystem -I$(GTEST_DIR)/include -I$(SRC_DIR)/include -I$(SRC_DIR) 8 | 9 | CXXFLAGS += -g -Wall -Wextra -Wno-unused-parameter -pthread -std=c++11 10 | 11 | LDLIBS=-lpam -lcurl -lldap -llber 12 | 13 | TESTS = test_config unit test_pam_oauth2_device 14 | 15 | GTEST_HEADERS = $(GTEST_DIR)/include/gtest/*.h \ 16 | $(GTEST_DIR)/include/gtest/internal/*.h 17 | 18 | objects = $(SRC_DIR)/pam_oauth2_device.o \ 19 | $(SRC_DIR)/include/config.o \ 20 | $(SRC_DIR)/include/ldapquery.o \ 21 | $(SRC_DIR)/include/metadata.o \ 22 | $(SRC_DIR)/include/nayuki/BitBuffer.o \ 23 | $(SRC_DIR)/include/nayuki/QrCode.o \ 24 | $(SRC_DIR)/include/nayuki/QrSegment.o \ 25 | $(SRC_DIR)/include/pam_oauth2_curl.o \ 26 | $(SRC_DIR)/include/pam_oauth2_log.o 27 | 28 | all: $(TESTS) 29 | for test in $(TESTS); do ./$${test}; done 30 | 31 | 32 | .PHONY: clean distclean 33 | 34 | clean: 35 | rm -f gtest.a gtest_main.a *.o $(objects) unit.o temp_file.o test_pam_oauth2_device.o 36 | 37 | distclean: clean 38 | rm -f $(TESTS) 39 | 40 | # System libraries for gtest (includes the default main function) 41 | GTEST_LIBS = -lgtest_main -lgtest -lpthread 42 | 43 | %.o: %.c %.h 44 | $(CXX) $(CXXFLAGS) -c $< -o $@ 45 | 46 | test_config.o: test_config.cpp $(GTEST_HEADERS) $(SRC_DIR)/include/config.hpp 47 | $(CXX) $(CPPFLAGS) $(CXXFLAGS) -I$(SRC_DIR) -c test_config.cpp 48 | 49 | test_config: test_config.o $(SRC_DIR)/include/config.o 50 | $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(GTEST_LIBS) $^ -o $@ 51 | 52 | unit: unit.o temp_file.o $(objects) 53 | $(CXX) $(CPPFLAGS) $(CXXFLAGS) $^ $(LDLIBS) $(GTEST_LIBS) -o $@ 54 | 55 | test_pam_oauth2_device.o: test_pam_oauth2_device.cpp $(GTEST_HEADERS) $(SRC_DIR)/include/config.hpp $(SRC_DIR)/pam_oauth2_device.hpp 56 | $(CXX) $(CPPFLAGS) $(CXXFLAGS) -I$(SRC_DIR) $(LDLIBS) -c test_pam_oauth2_device.cpp 57 | 58 | test_pam_oauth2_device: test_pam_oauth2_device.o $(objects) 59 | $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(GTEST_LIBS) $^ $(LDLIBS) -o $@ 60 | 61 | # unit tests, not part of the standard test suite (which tests pam_oauth_device.o itself) 62 | test_pam_oauth2_curl: test_pam_oauth2_curl.cpp ../src/include/pam_oauth2_curl.o 63 | $(CXX) $(CPPFLAGS) $(CXXFLAGS) -o $@ -L $(SRC_DIR)/include $^ $(GTEST_LIBS) -lcurl 64 | 65 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # PAM module testing 2 | 3 | 1. Install Google test (the build assumes a standard system install) 4 | 2. Run mock server `./mock_server.py`. 5 | 3. In a new terminal window execute `make` to run the tests. 6 | -------------------------------------------------------------------------------- /test/data/qr1.0.txt: -------------------------------------------------------------------------------- 1 | █▀▀▀▀▀▀▀█▀▀█▀▀█▀▀▀▀▀▀▀█ 2 | █ █▀▀▀█ █▀▄ █▀█ █▀▀▀█ █ 3 | █ █   █ █▄▄▀▀▄█ █   █ █ 4 | █ ▀▀▀▀▀ █ █▀▄▀█ ▀▀▀▀▀ █ 5 | ██▀█▀█▀▀▀ ▀▀▄█▀▀▀█▀▀█▀█ 6 | ███ ▄ ▀▀ █  █ ▄█▀▄▀▄▄ █ 7 | █▀█▄▄ █▀   ▀ ▄▀▄▄▀ █ ▄█ 8 | █▀▀▀▀▀▀▀█ █ ▀█ █▀▀ █▀▄█ 9 | █ █▀▀▀█ █▄▄█▀ ▀██▀██ ▄█ 10 | █ █   █ █▄█ ▀█▀▀   ▀▄ █ 11 | █ ▀▀▀▀▀ █▄▀▀▄▄██▀▄▀█▀██ 12 | ███████████████████████ 13 | -------------------------------------------------------------------------------- /test/data/qr1.1.txt: -------------------------------------------------------------------------------- 1 | █▀▀▀▀▀▀▀█▀▀█▀▀█▀▀▀▀▀▀▀█ 2 | █ █▀▀▀█ █▀▄ █▀█ █▀▀▀█ █ 3 | █ █   █ █▄▄▀▀▄█ █   █ █ 4 | █ ▀▀▀▀▀ █ █▀▄▀█ ▀▀▀▀▀ █ 5 | ██▀█▀█▀▀▀ ▀▀▄█▀▀▀█▀▀█▀█ 6 | ███ ▄ ▀▀ █  █ ▄█▀▄▀▄▄ █ 7 | █▀█▄▄ █▀   ▀ ▄▀▄▄▀ █ ▄█ 8 | █▀▀▀▀▀▀▀█ █ ▀█ █▀▀ █▀▄█ 9 | █ █▀▀▀█ █▄▄█▀ ▀██▀██ ▄█ 10 | █ █   █ █▄█ ▀█▀▀   ▀▄ █ 11 | █ ▀▀▀▀▀ █▄▀▀▄▄██▀▄▀█▀██ 12 | ███████████████████████ 13 | -------------------------------------------------------------------------------- /test/data/qr1.2.txt: -------------------------------------------------------------------------------- 1 | █▀▀▀▀▀▀▀█▀██▀▀▀▀███▀▀▀▀▀▀▀█ 2 | █ █▀▀▀█ █▀▄▄▄█▄▄███ █▀▀▀█ █ 3 | █ █   █ █▀▄▄▀▄   ▄█ █   █ █ 4 | █ ▀▀▀▀▀ █▀▄ █▀█ ▄ █ ▀▀▀▀▀ █ 5 | ██████▀▀█▄▀▀  ██▀██▀█▀█▀█▀█ 6 | ██▀▀▄▀█▀▄██▄█▄  █▄▀█ ▄▀  ▀█ 7 | ██▀█▀▄▄▀▀█▀ ▄▀▄▄█▄  ▄█ █▄██ 8 | █ █▀█▀▀▀██▀▀█ ▄█▀▀▀▄ ▀▄█▀▀█ 9 | █ █▄ ▀ ▀ █ ▄▄█▄ ▀ ▀▀▀   ▀▄█ 10 | █▀▀▀▀▀▀▀█▄▀▄▄█▄▄▀ █▀█ ▄ █▀█ 11 | █ █▀▀▀█ █▄▀▄▄ ▀█▄ ▀▀▀  ▀███ 12 | █ █   █ ██▄ ▄▄ ▀█▀▄▀ ████ █ 13 | █ ▀▀▀▀▀ ██▀▄ █▄▄ ▀█ ▀▀▄██ █ 14 | ███████████████████████████ 15 | -------------------------------------------------------------------------------- /test/data/template_empty.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/data/template_noldap.json: -------------------------------------------------------------------------------- 1 | { 2 | "oauth": { 3 | "client": { 4 | "id": "client_id", 5 | "secret": "client_secret" 6 | }, 7 | "scope": "openid profile", 8 | "device_endpoint":"https://provider.com/devicecode", 9 | "token_endpoint": "https://provider.com/token", 10 | "userinfo_endpoint": "https://provider.com/userinfo", 11 | "username_attribute": "preferred_username" 12 | }, 13 | "qr": { 14 | "error_correction_level": 0 15 | }, 16 | "users": { 17 | "provider_user_id_1": 18 | [ 19 | "root", 20 | "bob" 21 | ], 22 | "provider_user_id_2": 23 | [ 24 | "mike" 25 | ] 26 | } 27 | } -------------------------------------------------------------------------------- /test/data/template_wrong.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stfc/pam_oauth2_device/483c3f3bb0b61805eebb733d9bbe74605b09970b/test/data/template_wrong.json -------------------------------------------------------------------------------- /test/mock_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import base64 4 | import json 5 | import re 6 | from http.server import HTTPServer, BaseHTTPRequestHandler 7 | from urllib.parse import parse_qs 8 | 9 | PORT = 8042 10 | 11 | class MockServerRequestHandler(BaseHTTPRequestHandler): 12 | 13 | DEVICECODE_PATTERN = re.compile(r'/devicecode') 14 | TOKEN_PATTERN = re.compile(r'/token') 15 | USERINFO_PATTERN = re.compile(r'/userinfo') 16 | CLIENT_ID = 'client_id' 17 | CLIENT_SECRET = 'NDVmODY1ZDczMGIyMTM1MWFlYWM2NmYw' 18 | SCOPE = 'openid profile' 19 | USER_CODE = 'QWERTY' 20 | DEVICE_CODE = 'e1e9b7be-e720-467e-bbe1-5c382356e4a9' 21 | ACCESS_TOKEN = 'ZjBhNTQxYzEzMGQwNWU1OWUxMDhkMTM5' 22 | VERIFICATION_URL = 'http://localhost:{}/oidc/device'.format(PORT) 23 | 24 | def do_GET(self): 25 | if re.search(self.USERINFO_PATTERN, self.path): 26 | if 'Bearer ' + self.ACCESS_TOKEN in self.headers.get('Authorization', ''): 27 | response_data = { 28 | 'sub': 'YzQ4YWIzMzJhZjc5OWFkMzgwNmEwM2M5', 29 | 'preferred_username': 'jdoe', 30 | 'name': 'Joe Doe' 31 | } 32 | self.send_response(200) 33 | self.end_headers() 34 | self.wfile.write(json.dumps(response_data).encode()) 35 | else: 36 | self.send_response(403) 37 | self.end_headers() 38 | else: 39 | self.send_response(404) 40 | self.end_headers() 41 | def do_POST(self): 42 | body = self.rfile.read(int(self.headers['Content-Length'])).decode() 43 | post_data = parse_qs(body) 44 | if re.search(self.DEVICECODE_PATTERN, self.path): 45 | if (post_data['client_id'] == [self.CLIENT_ID] and 46 | post_data['scope'] == [self.SCOPE]): 47 | response_data = { 48 | 'user_code': self.USER_CODE, 49 | 'verification_uri': self.VERIFICATION_URL, 50 | 'verification_uri_complete': '{}?user_code={}'.format( 51 | self.VERIFICATION_URL, self.DEVICE_CODE), 52 | 'device_code': self.DEVICE_CODE, 53 | 'error': None, 54 | 'expires_in': 1800 55 | } 56 | self.send_response(200) 57 | self.end_headers() 58 | self.wfile.write(json.dumps(response_data).encode()) 59 | else: 60 | self.send_response(403) 61 | self.end_headers() 62 | elif re.search(self.TOKEN_PATTERN, self.path): 63 | auth = self.headers.get('Authorization', '') 64 | if (post_data['client_id'] == [self.CLIENT_ID] and 65 | post_data['device_code'] == [self.DEVICE_CODE] and 66 | post_data['grant_type'] == ['urn:ietf:params:oauth:grant-type:device_code'] and 67 | 'Basic' in auth and 68 | base64.b64decode(auth.split()[1]).decode() == '{}:{}'.format( 69 | self.CLIENT_ID, self.CLIENT_SECRET)): 70 | response_data = { 71 | 'access_token': self.ACCESS_TOKEN, 72 | 'error': None, 73 | 'expires_in': 3600, 74 | 'scope': self.SCOPE, 75 | 'token_type': 'Bearer' 76 | } 77 | self.send_response(200) 78 | self.end_headers() 79 | self.wfile.write(json.dumps(response_data).encode()) 80 | else: 81 | self.send_response(403) 82 | self.end_headers() 83 | else: 84 | self.send_response(404) 85 | self.end_headers() 86 | 87 | 88 | if __name__ == '__main__': 89 | try: 90 | httpd = HTTPServer(('localhost', PORT), MockServerRequestHandler) 91 | httpd.serve_forever() 92 | except KeyboardInterrupt: 93 | httpd.shutdown() 94 | print() -------------------------------------------------------------------------------- /test/temp_file.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jens.jen@stfc.ac.uk on 25/06/2021. 3 | // Fairly Unix specific, but this was just designed to be a simple RAII file 4 | // 5 | // TODO tidy up all the different errors and exceptions 6 | // TODO handle case where a named file overwrites an existing file 7 | // TODO check for thread safety 8 | // TODO allocating FILENAME_MAX for every instance is a bit wasteful... 9 | 10 | #include "temp_file.hpp" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | //#include 17 | 18 | 19 | // As it says on the label. Returns true if successful. 20 | static bool write_data_to_file(FILE *fp, char const *data) noexcept; 21 | 22 | // very C-ish 23 | static void make_temp( char *filename, size_t size, FILE **file ) 24 | { 25 | constexpr char const *tempname = "/tmp/pam_oauth2_XXXXXX"; // must have six Xs 26 | if(strlen(tempname) >= size) 27 | throw "Really can't happen"; 28 | strncpy(filename, tempname, size); 29 | int fd = mkstemp(filename); 30 | if(fd < 0) 31 | throw "Failed to create temp file"; 32 | *file = fdopen(fd, "w"); // foo inherits the file descriptor 33 | if(!*file) 34 | throw "Failed to create file object (out of memory?)"; 35 | } 36 | 37 | 38 | static void save_cwd( char *filename, size_t size ) 39 | { 40 | if (!getcwd(filename, size)) 41 | throw "Insufficient memory"; // can't happen 42 | } 43 | 44 | 45 | 46 | static void save_filename(char const *filename, char *dest, size_t size) 47 | { 48 | char *p = dest; 49 | if(*filename != '/') { 50 | // For a relative path, save the current directory path (in which the relative path is created) 51 | // (unless another thread changes it between the getcwd and the file creation?!) 52 | save_cwd( dest, size ); 53 | size_t len = strlen(p); 54 | // Add '/' at the end of the path, so we can concatenate the filename 55 | dest[len] = '/'; 56 | dest[++len] = '\0'; 57 | p += len; 58 | size -= len; 59 | } 60 | if (strlen(filename) >= size) 61 | throw "Filename too long"; 62 | strncpy(p, filename, size); 63 | } 64 | 65 | 66 | 67 | 68 | 69 | TempFile::TempFile(const char *contents) 70 | { 71 | FILE *foo; 72 | make_temp(fname_, sizeof(fname_), &foo); 73 | if(!write_data_to_file(foo, contents)) { 74 | fclose(foo); 75 | unlink(fname_); 76 | throw "Unable to write data to file"; 77 | } 78 | fclose(foo); 79 | } 80 | 81 | 82 | 83 | // We don't have stringview or filesystem until C++17 84 | TempFile::TempFile(std::string const &s) 85 | { 86 | FILE *foo; 87 | make_temp(fname_, sizeof(fname_), &foo); 88 | bool did_it_work_eh = write_data_to_file(foo, s.c_str()); 89 | fclose(foo); 90 | if(!did_it_work_eh) { 91 | unlink(fname_); 92 | throw "Failed to write data to file"; 93 | } 94 | } 95 | 96 | 97 | 98 | 99 | TempFile::TempFile(char const *filename, std::string const &contents) 100 | { 101 | save_filename(filename, fname_, sizeof(fname_)); 102 | FILE *foo = fopen(fname_, "w"); 103 | if(!foo) 104 | throw "Failed to open file for writing"; 105 | bool did_it_work_eh = write_data_to_file(foo, contents.c_str()); 106 | fclose(foo); 107 | if(!did_it_work_eh) { 108 | unlink(fname_); 109 | throw "Failed to write data to file"; 110 | } 111 | } 112 | 113 | 114 | 115 | TempFile::TempFile(const char *filename, const char *contents) 116 | { 117 | save_filename(filename, fname_, sizeof(fname_)); 118 | FILE *f = fopen(filename, "w"); 119 | if(!f) 120 | throw "failed to create file for writing"; 121 | if(!write_data_to_file(f, contents)) { 122 | fclose(f); 123 | unlink(filename); 124 | throw "Failed to write data to file"; 125 | } 126 | fclose(f); 127 | } 128 | 129 | 130 | TempFile::~TempFile() 131 | { 132 | // RAII 133 | unlink(fname_); 134 | } 135 | 136 | 137 | std::string 138 | TempFile::filename() const 139 | { 140 | return std::string{fname_}; 141 | } 142 | 143 | 144 | // TODO CWD could change for relative named files 145 | std::string 146 | TempFile::dirname() const 147 | { 148 | char const *p = fname_, *q = strrchr(fname_, '/'); 149 | if(q) { 150 | std::string path(fname_, q-p); 151 | return path; 152 | } 153 | // can't happen: path wasn't set, so return CWD 154 | char buf[FILENAME_MAX]; 155 | if(!getcwd(buf, FILENAME_MAX)) 156 | throw std::bad_alloc(); // can't happen 157 | return std::string(buf); 158 | } 159 | 160 | 161 | bool 162 | write_data_to_file(FILE *fp, char const *data) noexcept 163 | { 164 | size_t len = strlen(data); 165 | return len == fwrite(data, 1, len, fp); 166 | } 167 | -------------------------------------------------------------------------------- /test/temp_file.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jens.jensen@stfc.ac.uk on 25/06/2021. 3 | // 4 | // This class uses RAII to create a temporary file with a specific content. 5 | // This is used to create files for testing which are automatically cleared after the test has finished. 6 | // It really should use boost or something for portable file handling but it needs to minimise dependencies 7 | 8 | 9 | #ifndef __PAM_OAUTH2_DEVICE_TEMP_FILE_HPP 10 | #define __PAM_OAUTH2_DEVICE_TEMP_FILE_HPP 11 | 12 | #include 13 | #include 14 | 15 | /** \brief Create a temporary file with specified content using RAII */ 16 | 17 | class TempFile { 18 | private: 19 | char fname_[FILENAME_MAX]; 20 | public: 21 | /** @brief construct file with given contents */ 22 | TempFile(std::string const &contents); 23 | /** @brief construct file with given contents */ 24 | TempFile(char const *contents); 25 | /** @brief construct file with given name and contents */ 26 | TempFile(char const *filename, char const *contents); 27 | /** @brief construct file with given name and contents */ 28 | TempFile(char const *filename, std::string const &contents); 29 | 30 | TempFile(TempFile const &) = delete; 31 | TempFile(TempFile &&) = delete; 32 | TempFile operator=(TempFile const &) = delete; 33 | TempFile &operator=(TempFile &&) = delete; 34 | ~TempFile(); 35 | 36 | /** Return the full path/name of the file */ 37 | std::string filename() const; 38 | /** Return the directory of the file */ 39 | std::string dirname() const; 40 | }; 41 | 42 | 43 | #endif //__PAM_OAUTH2_DEVICE_TEMP_FILE_HPP 44 | -------------------------------------------------------------------------------- /test/test_config.cpp: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | #include "include/config.hpp" 3 | #include "include/nlohmann/json.hpp" 4 | 5 | #define CLIENT_ID "client_id" 6 | 7 | using json = nlohmann::json; 8 | 9 | namespace 10 | { 11 | 12 | TEST(ConfigTest, MissingFile) 13 | { 14 | Config config; 15 | ASSERT_THROW(config.load("data/missing.json"), json::parse_error); 16 | } 17 | 18 | TEST(ConfigTest, WrongFormat) 19 | { 20 | Config config; 21 | ASSERT_THROW(config.load("data/template_wrong.json"), json::parse_error); 22 | } 23 | 24 | TEST(ConfigTest, Empty) 25 | { 26 | Config config; 27 | ASSERT_THROW(config.load("data/template_empty.json"), json::out_of_range); 28 | } 29 | 30 | TEST(ConfigTest, NoLdap) 31 | { 32 | Config config; 33 | config.load("data/template_noldap.json"); 34 | EXPECT_EQ(config.client_id, CLIENT_ID); 35 | EXPECT_TRUE(config.ldap_host.empty()); 36 | } 37 | 38 | TEST(ConfigTest, Full) 39 | { 40 | Config config; 41 | config.load("../config_template.json"); 42 | EXPECT_EQ(config.client_id, CLIENT_ID); 43 | EXPECT_EQ(config.ldap_host, "ldaps://ldap-server:636"); 44 | EXPECT_EQ(config.usermap["provider_user_id_1"].count("root"), 1); 45 | EXPECT_EQ(config.usermap.size(), 2); 46 | EXPECT_EQ(config.qr_error_correction_level, 0); 47 | } 48 | 49 | } // namespace -------------------------------------------------------------------------------- /test/test_pam_oauth2_device.cpp: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | #include "pam_oauth2_device.hpp" 3 | 4 | 5 | #define DEVICE_ENDPOINT "http://localhost:8042/devicecode" 6 | #define TOKEN_ENDPOINT "http://localhost:8042/token" 7 | #define USERINFO_ENDPOINT "http://localhost:8042/userinfo" 8 | #define USERNAME_ATTRIBUTE "preferred_username" 9 | #define CLIENT_ID "client_id" 10 | #define CLIENT_SECRET "NDVmODY1ZDczMGIyMTM1MWFlYWM2NmYw" 11 | #define SCOPE "openid profile" 12 | #define USER_CODE "QWERTY" 13 | #define DEVICE_CODE "e1e9b7be-e720-467e-bbe1-5c382356e4a9" 14 | #define ACCESS_TOKEN "ZjBhNTQxYzEzMGQwNWU1OWUxMDhkMTM5" 15 | #define VERIFICATION_URL "http://localhost:8042/oidc/device" 16 | 17 | namespace 18 | { 19 | 20 | TEST(PamTest, Device) 21 | { 22 | Config config; 23 | pam_oauth2_log logger; 24 | DeviceAuthResponse response; 25 | make_authorization_request(config, 26 | logger, 27 | CLIENT_ID, 28 | CLIENT_SECRET, 29 | SCOPE, 30 | DEVICE_ENDPOINT, 31 | &response); 32 | EXPECT_EQ(response.user_code, USER_CODE); 33 | EXPECT_EQ(response.device_code, DEVICE_CODE); 34 | EXPECT_EQ(response.verification_uri, VERIFICATION_URL); 35 | EXPECT_EQ(response.verification_uri_complete, 36 | std::string(VERIFICATION_URL) + "?user_code=" + DEVICE_CODE); 37 | } 38 | 39 | TEST(PamTest, Token) 40 | { 41 | std::string token; 42 | poll_for_token(CLIENT_ID, CLIENT_SECRET, 43 | TOKEN_ENDPOINT, 44 | DEVICE_CODE, token); 45 | EXPECT_EQ(token, ACCESS_TOKEN); 46 | } 47 | 48 | TEST(PamTest, Userinfo) 49 | { 50 | Userinfo userinfo; 51 | get_userinfo(USERINFO_ENDPOINT, 52 | ACCESS_TOKEN, 53 | USERNAME_ATTRIBUTE, 54 | &userinfo); 55 | EXPECT_EQ(userinfo.sub, "YzQ4YWIzMzJhZjc5OWFkMzgwNmEwM2M5"); 56 | EXPECT_EQ(userinfo.username, "jdoe"); 57 | EXPECT_EQ(userinfo.name, "Joe Doe"); 58 | } 59 | 60 | } // namespace -------------------------------------------------------------------------------- /test/unit.cpp: -------------------------------------------------------------------------------- 1 | /************************************************************ 2 | * *** Unit and correctness testing for pam_oauth2_device 3 | * Normally we test the public API but in this case we need to test the private API as well 4 | * jens.jensen@stfc.ac.uk 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "config.hpp" 12 | #include "metadata.hpp" 13 | #include "pam_oauth2_device.hpp" 14 | #include "temp_file.hpp" 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | 21 | /** \brief Check whether the contents of a file matches exactly that of the string being passed to it 22 | * \param filename - the name of the file to be scanned relative to CWD 23 | * \param string - the string to compare 24 | * @return -1 if matching, -1000 if file is absent; or location of first mismatch if not matching 25 | */ 26 | ssize_t cmp_file_string(char const *filename, std::string const &string); 27 | 28 | enum class ConfigSection { TEST_CLOUD, TEST_GROUP, TEST_USERMAP, TEST_LDAP }; 29 | 30 | /** \brief Make a dummy Config class for testing */ 31 | Config make_dummy_config(ConfigSection, Userinfo const &); 32 | 33 | /** \brief make a dummy userinfo class */ 34 | Userinfo make_dummy_userinfo(std::string const &username); 35 | 36 | /** Test function for cloud section of is_authorized() */ 37 | bool is_authorized_cloud(Userinfo &ui, std::string const &username_local, std::vector const &groups); 38 | 39 | /** Test function for group section of is_authorized() */ 40 | bool is_authorized_group(Userinfo &ui, std::string const &username_local, std::string const &service_name, std::vector const &groups); 41 | 42 | /** Test function for the local usermap section of is_authorized (mapping remote usernames to authorised local usernames */ 43 | bool is_authorized_local(Userinfo &ui, std::string const &username_local); 44 | 45 | /* copied prototypes for private (compilation unit) functions from pam_oauth2_device.cpp */ 46 | std::string getQr(const char *text, const int ecc = 0, const int border = 1); 47 | 48 | class DeviceAuthResponse; 49 | 50 | void make_authorization_request(Config const &, 51 | pam_oauth2_log &, 52 | std::string const &client_id, 53 | std::string const &client_secret, 54 | std::string const &scope, 55 | std::string const &device_endpoint, 56 | DeviceAuthResponse *response); 57 | 58 | void poll_for_token(Config const &config, 59 | pam_oauth2_log &logger, 60 | std::string const &client_id, 61 | std::string const &client_secret, 62 | std::string const &token_endpoint, 63 | std::string const &device_code, 64 | std::string &token); 65 | 66 | Userinfo get_userinfo(Config const &config, 67 | pam_oauth2_log &logger, 68 | std::string const &userinfo_endpoint, 69 | std::string const &token, 70 | std::string const &username_attribute); 71 | 72 | void show_prompt(pam_handle_t *pamh, 73 | int qr_error_correction_level, 74 | DeviceAuthResponse *device_auth_response); 75 | 76 | bool is_authorized(Config const &config, 77 | pam_oauth2_log &logger, 78 | std::string const &username_local, 79 | Userinfo const &userinfo, 80 | char const *metadata_path = nullptr); 81 | 82 | 83 | TEST(PamOAuth2Unit, QrCodeTest) 84 | { 85 | char const *text = "I want to think audibly this evening. I do not want to make a speech and if you find me this evening speaking without reserve, pray, consider" 86 | " that you are only sharing the thoughts of a man who allows himself to think audibly, and if you think that I seem to transgress the limits" 87 | " that courtesy imposes upon me, pardon me for the liberty I may be taking."; 88 | char const *loremipsum = "loremipsum"; 89 | EXPECT_EQ(cmp_file_string("data/qr1.0.txt", getQr(loremipsum, 0, 1)), -1); 90 | EXPECT_EQ(cmp_file_string("data/qr1.1.txt", getQr(loremipsum, 1, 1)), -1); 91 | EXPECT_EQ(cmp_file_string("data/qr1.2.txt", getQr(loremipsum, 2, 1)), -1); 92 | EXPECT_EQ(cmp_file_string("data/qr2.0.txt", getQr(text, 0, 1)), -1); 93 | EXPECT_EQ(cmp_file_string("data/qr2.1.txt", getQr(text, 1, 1)), -1); 94 | EXPECT_EQ(cmp_file_string("data/qr2.2.txt", getQr(text, 2, 1)), -1); 95 | } 96 | 97 | TEST(PamOAuth2Unit, IsAuthorized) 98 | { 99 | // Userinfo contains the remote username. Note the suffix is (or will be) configured as ".test" to match a local username "fred" 100 | Userinfo ui{make_dummy_userinfo("fred.test")}; 101 | // groups denotes the groups assigned to the project id 102 | std::vector groups; 103 | // No or wrong groups, right username 104 | EXPECT_TRUE( !is_authorized_cloud(ui, "fred", groups)); 105 | groups.push_back("sknamp"); 106 | EXPECT_TRUE( !is_authorized_cloud(ui, "fred", groups)); 107 | // Groups, correct username 108 | groups.push_back("bleps"); 109 | groups.push_back("plamf"); 110 | // Check groups is sorted 111 | std::sort(groups.begin(), groups.end()); 112 | // One group is right, username is right 113 | EXPECT_TRUE( is_authorized_cloud(ui, "fred", groups)); 114 | // Right groups, wrong username 115 | EXPECT_TRUE( !is_authorized_cloud(ui, "barney", groups)); 116 | // Now for the groups test, starting with a service name which is not one of fred's groups 117 | EXPECT_TRUE(!is_authorized_group(ui, "fred", "bylzp", groups)); 118 | // service name is one of fred's Userinfo groups but not in groups, and but username is different 119 | EXPECT_TRUE(!is_authorized_group(ui, "wilma", "plempf", groups)); 120 | // service name is one of fred's Userinfo groups but not in project_id groups 121 | EXPECT_TRUE(is_authorized_group(ui, "fred", "plempf", groups)); 122 | // service name is in project_id groups but not in fred's Userinfo groups 123 | EXPECT_TRUE(!is_authorized_group(ui, "fred", "plamf", groups)); 124 | // local map test: remote name is in the list but local name doesn't match 125 | EXPECT_TRUE(!is_authorized_local(ui, "gnumpf")); 126 | // local map test: remote name is in the list and a local name matches 127 | EXPECT_TRUE(is_authorized_local(ui, "fred")); 128 | // local map test: remote name is not in list 129 | Userinfo ui2{"0123456789abcdef", "barney.test", "barney"}; 130 | EXPECT_TRUE(!is_authorized_local(ui2, "barney")); 131 | } 132 | 133 | 134 | ssize_t 135 | cmp_file_string(char const *filename, std::string const &string) 136 | { 137 | std::ifstream foo(filename, std::ios_base::binary); 138 | if(!foo) 139 | return -1000; 140 | ssize_t index = 0; 141 | // istream_iterator doesn't work because it parses the input 142 | auto p = string.cbegin(); 143 | auto const q = string.cend(); 144 | while(p != q) { 145 | // get returns EOF at, er, EOF, and EOF is never a character 146 | char c1, c2; 147 | c1 = foo.get(); 148 | c2 = *p++; 149 | if(c1 != c2) { 150 | return index; 151 | } 152 | } 153 | return -1; // match 154 | } 155 | 156 | 157 | 158 | Config 159 | make_dummy_config(ConfigSection section, Userinfo const &ui) 160 | { 161 | Config cf; 162 | // All members are public! and have no non-default initialisers 163 | // Boolean selectors of test section 164 | cf.cloud_access = cf.group_access = false; 165 | cf.local_username_suffix = ".test"; 166 | switch (section) { 167 | case ConfigSection::TEST_CLOUD: 168 | cf.cloud_access = true; 169 | // The following three variables are needed: cloud_username, local_username_suffix, cloud_endpoint 170 | // "cloud username" is the remote username 171 | cf.cloud_username = "fred.test"; 172 | // endpoint is set later as we don't know it yet 173 | // metadata_file is set later as we don't know it yet 174 | break; 175 | case ConfigSection::TEST_GROUP: 176 | cf.group_access = true; 177 | break; 178 | case ConfigSection::TEST_LDAP: 179 | break; 180 | case ConfigSection::TEST_USERMAP: 181 | // create a dummy usermap to test against 182 | std::set fred{"fred", "blips", "flopsy"}; 183 | std::set wilma{"wilma", "betty", "blaps"}; 184 | cf.usermap.insert(std::pair>{"fred.test", fred}); 185 | cf.usermap.insert(std::pair>{"wilma.test", wilma}); 186 | break; 187 | // no default 188 | } 189 | return cf; 190 | } 191 | 192 | 193 | Userinfo 194 | make_dummy_userinfo(std::string const &username) 195 | { 196 | Userinfo ui{"0123456789abcdef", username, "jdoe"}; 197 | // Note groups are not added alphabetically 198 | ui.add_group("splomp"); 199 | ui.add_group("plempf"); 200 | ui.add_group("bleps"); 201 | return ui; 202 | } 203 | 204 | 205 | 206 | Metadata 207 | make_dummy_metadata() 208 | { 209 | Metadata md; 210 | // This is currently a public member! but will not test the load function 211 | md.project_id = "iristest"; 212 | return md; 213 | } 214 | 215 | 216 | std::string 217 | make_groups_json(std::vector const &groups) 218 | { 219 | // Slightly hacky JSON construction 220 | std::string contents{"{\"groups\":[\""}; 221 | if(!groups.empty()) { 222 | auto end = groups.cend()-1; 223 | std::for_each(groups.cbegin(), end, [&contents](std::string const &grp) { contents += grp; contents += "\",\""; }); 224 | contents += *end; 225 | } 226 | contents += "\"]}"; 227 | return contents; 228 | } 229 | 230 | 231 | bool 232 | is_authorized_cloud(Userinfo &ui, std::string const &username_local, std::vector const &groups) 233 | { 234 | Config cf{make_dummy_config(ConfigSection::TEST_CLOUD, ui)}; 235 | pam_oauth2_log log(nullptr, pam_oauth2_log::log_level_t::DEBUG); 236 | TempFile metadata("{\"project_id\":\"iristest\"}"); 237 | cf.metadata_file = metadata.filename(); 238 | // The project id is the name of the file 239 | TempFile cloud( "iristest", make_groups_json(groups)); 240 | // curl can read a local file! 241 | cf.cloud_endpoint = "file://" + cloud.dirname(); 242 | // The project id file should be passed in with the config. 243 | // Destructors are not called until the call has returned (so c_str()s are safe) 244 | return is_authorized(cf, log, username_local, ui); 245 | } 246 | 247 | 248 | 249 | bool 250 | is_authorized_group(Userinfo &ui, std::string const &username_local, std::string const &service_name, std::vector const &groups) 251 | { 252 | Config cf{make_dummy_config(ConfigSection::TEST_GROUP, ui)}; 253 | pam_oauth2_log log(nullptr, pam_oauth2_log::log_level_t::DEBUG); 254 | cf.group_service_name = service_name; // gets copied (string constructor) 255 | return is_authorized(cf, log, username_local, ui, nullptr /* only needed for cloud */); 256 | } 257 | 258 | 259 | 260 | bool 261 | is_authorized_local(Userinfo &ui, std::string const &username_local) 262 | { 263 | Config cf{make_dummy_config(ConfigSection::TEST_USERMAP, ui)}; 264 | pam_oauth2_log log(nullptr, pam_oauth2_log::log_level_t::DEBUG); 265 | return is_authorized(cf, log, username_local, ui, nullptr); 266 | } 267 | -------------------------------------------------------------------------------- /util/tls-debug/README.md: -------------------------------------------------------------------------------- 1 | # tls-debug 2 | 3 | Small utility using curl to debug connecting to a remote server using SSL/TLS. It could be https as when 4 | connecting to an IAM server, or it could be ldaps if connecting to a secure LDAP endpoint. Or indeed any 5 | other protocol for which curl supports TLS. 6 | 7 | ## Build 8 | 9 | The only dependency is libcurl (including the headers (dev) package for building the utility), but of course 10 | curl itself will have further dependencies. 11 | 12 | ``` 13 | gcc -Wall -o tls-debug tls-debug.c -lcurl 14 | ``` 15 | 16 | ## Run 17 | 18 | ``` 19 | ./tls-debug trustanchors.pem https://iam-host.example.com/ 20 | ``` 21 | 22 | Here `trustanchors.pem` can be a file (as in this example) with multiple trust anchor (certification 23 | authority) certificates concatenated together, or it can be a directory as used by OpenSSL. If it is a 24 | directory, the connection will work only if curl uses OpenSSL. 25 | 26 | ## Options 27 | 28 | The utility understands no options whatsoever. Debug is what you get and debug is all you get. 29 | 30 | ## TODO 31 | 32 | The next obvious step is to add client credentials as the third command line parameter. 33 | -------------------------------------------------------------------------------- /util/tls-debug/tls-debug.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include /* strerror */ 8 | #include 9 | 10 | 11 | /** Max number of bytes to fetch from server */ 12 | 13 | #define MAX_DATA 1200 14 | 15 | 16 | /** setup curl 17 | * \param uninitialised curl ptr 18 | * \param trust anchor path 19 | * \return errno or 0 if successful 20 | */ 21 | 22 | int setup(CURL **, char const *ta_path); 23 | 24 | 25 | 26 | struct call_data { 27 | char errbuf[CURL_ERROR_SIZE]; 28 | char data[MAX_DATA]; 29 | size_t fill; 30 | }; 31 | 32 | 33 | /** call a URL 34 | * \param initialised curl structure 35 | * \param url to connect to 36 | * \param uninitialised call_data structure will be filled in 37 | * \return curl error code 38 | */ 39 | 40 | CURLcode call(CURL *curl, char const *url, struct call_data *); 41 | 42 | 43 | int 44 | main(int argc, char **argv) 45 | { 46 | CURL *curl = 0; 47 | struct call_data data; 48 | // Check if we have a trust anchor 49 | if(argc != 3) { 50 | fprintf(stderr, "Usage: %s \n", argv[0]); 51 | exit(1); 52 | } 53 | // Set up curl and configure the trust anchor 54 | int ret = setup(&curl, argv[1]); 55 | if(ret != 0) { 56 | fprintf(stderr, "Setup error: %s\n", strerror(ret)); 57 | exit(1); 58 | } 59 | if((ret = call(curl, argv[2], &data)) != 0) { 60 | fprintf(stderr, "Failed call to %s: %s\n%s\n", 61 | argv[2], 62 | curl_easy_strerror(ret), 63 | data.errbuf); 64 | return 1; 65 | } 66 | return 0; 67 | } 68 | 69 | 70 | 71 | 72 | size_t 73 | callback(char const *contents, size_t size, size_t nmemb, void *user) 74 | { 75 | struct call_data *data = (struct call_data *)user; 76 | size_t to_read = size*nmemb; 77 | if(to_read > MAX_DATA - data->fill) 78 | to_read = MAX_DATA - data->fill; 79 | if(to_read > 0) { 80 | memcpy(&(data->data[data->fill]), contents, to_read); 81 | data->fill += to_read; 82 | } 83 | return to_read; 84 | } 85 | 86 | 87 | int 88 | setup(CURL **curl, char const *ta_path) 89 | { 90 | struct stat fs; 91 | if(stat(ta_path, &fs)) 92 | return errno; 93 | *curl = curl_easy_init(); 94 | if(!*curl) { 95 | fprintf(stderr, "Cannot init curl\n"); 96 | return EINVAL; 97 | } 98 | 99 | if(curl_easy_setopt(*curl, CURLOPT_SSL_VERIFYPEER, 1L) != CURLE_OK) { 100 | fprintf(stderr, "Failed to set verify peer flag\n"); 101 | return EINVAL; 102 | } 103 | 104 | if(curl_easy_setopt(*curl, CURLOPT_SSL_VERIFYHOST, 2L) != CURLE_OK) { 105 | fprintf(stderr, "Failed to set verify peer flag\n"); 106 | return EINVAL; 107 | } 108 | 109 | 110 | switch(fs.st_mode & S_IFMT) { 111 | case S_IFREG: 112 | if(curl_easy_setopt(*curl, CURLOPT_CAINFO, ta_path) != CURLE_OK) { 113 | fprintf(stderr, "Failed to set CA **bundle** to %s\n", ta_path); 114 | return EINVAL; 115 | } 116 | break; 117 | case S_IFDIR: 118 | if(curl_easy_setopt(*curl, CURLOPT_CAPATH, ta_path) != CURLE_OK) { 119 | fprintf(stderr, "Failed to set CA **path** to %s\n", ta_path); 120 | return EINVAL; 121 | } 122 | break; 123 | default: 124 | fprintf(stderr, "Unknown or unsupported data type %s: %d\n", ta_path, fs.st_mode & S_IFMT); 125 | return EINVAL; 126 | } 127 | 128 | return 0; 129 | } 130 | 131 | 132 | 133 | CURLcode 134 | call(CURL *curl, char const *url, struct call_data *data) 135 | { 136 | data->fill = 0; 137 | CURLcode ret; 138 | if((ret = curl_easy_setopt(curl, CURLOPT_URL, url)) != CURLE_OK) { 139 | fputs("Failed to set URL\n", stderr); 140 | return ret; 141 | } 142 | if((ret = curl_easy_setopt(curl, CURLOPT_VERBOSE, 1)) != CURLE_OK) { 143 | fputs("Warning: failed to set verbose\n", stderr); 144 | } 145 | if((ret = curl_easy_setopt(curl, CURLOPT_HEADER, 1)) != CURLE_OK) { 146 | fputs("Warning: failed to set header out\n", stderr); 147 | } 148 | if((ret = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, callback)) != CURLE_OK) { 149 | fputs("Failed to set callback function\n", stderr); 150 | return ret; 151 | } 152 | if((ret = curl_easy_setopt(curl, CURLOPT_WRITEDATA, data->data)) != CURLE_OK) { 153 | fputs("Failed to set callback buffer\n", stderr); 154 | return ret; 155 | } 156 | 157 | /* The Moment of Truth... */ 158 | return curl_easy_perform(curl); 159 | } 160 | --------------------------------------------------------------------------------