├── .gitignore ├── LICENSE ├── README.md └── gpg_key.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,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 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 netson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ansible-gpg-key 2 | Module to manage GPG keys from files and keyservers. 3 | 4 | ## Introduction 5 | Inspired by TNT (https://github.com/tnt/ansible-gpg-import-module), I created this module from scratch to better suit my needs. It allows you to manage GPG keys on a ansible managed target host. You can either provide keys (both public and secret) via keyfiles on the target or on the host or you can download keyfiles from keyservers. It also allows you to define a custom trust level for each key or retrieve info on installed keys. 6 | 7 | ### Ansible secrets lookup and generation module 8 | 9 | Looking for an easy and secure way to generate and store GPG keys for your ansible hosts? Check out my secrets lookup module for ansible: https://github.com/netson/ahvl 10 | 11 | ## Requirements 12 | 13 | This module was created for use with GnuPG **v2.2.4+** (including libgcrypt **v1.8.1+**), which is the default on Ubuntu 18.04 images. The libgcrypt version is also checked because I also use Ed25519 keys, which is supported only by newer versions of libgcrypt. The module may also work on older versions, but it is untested. Also, some commands have changed between GnuGP version 1.x and 2.x and therefore are incompatible. 14 | 15 | To perform this version check, make sure python package ```packaging``` is installed on the target host: 16 | ```bash 17 | pip install packaging 18 | ``` 19 | 20 | ## Installation 21 | 22 | To install this module, clone it, download it, copy it, whatever (but name the file **gpg_key.py**), to a folder on your ansible host. Then, make sure ansible can find the module by pointing ```library``` to the folder containing the gpg_key.py file: 23 | 24 | ``` 25 | # set modules path, seperate with colons if multiple paths 26 | library = /path/to/my/modules 27 | ``` 28 | 29 | ## Options 30 | 31 | Providing either **fpr**, **file** or **content** is required 32 | 33 | | Option | Required | Type | Choices | Default | Description | 34 | |--------|----------|------|---------|---------|-------------| 35 | | **fpr** | ```False``` | ```str``` | | | Key Fingerprint to install from keyserver, to delete from target machine, or to get info on. To get info on all installed keys, use * as the value for fpr. Using any shorter ID than the full fingerprint will fail. Using the short ID's isn't recommended anyways, due to possible collisions. | 36 | | **keyserver** | | ```str``` | | ```keyserver.ubuntu.com``` | Keyserver to download key from | 37 | | **file** | ```False``` | ```path``` | | | File on target machine containing the key(s) to install; be aware that a file can contain more than 1 key; if this is the case, all keys will be imported and all keys will receive the same trust level. The module auto-detects if the given key is a public or secret key. | 38 | | **content** | ```False``` | ```str``` | | | Contents of keyfile to install on target machine just like the file, the contents can contain more than 1 key and all keys will receive the same trust level. The module auto-detects if the given key is a public or secret key. The content parameter simply creates a temporary file on the target host and then performs the same actions as the file parameter. It is just an easy method to not have to create a keyfile on the target machine first. | 39 | | **manage_trust** | | ```bool``` | | ```True``` | Setting controls wether or not the module controls the trust levels of the (imported) keys. If set to false, no changes will be made to the trust level regardless of the 'trust' setting. | 40 | | **trust** | | ```str``` | ```[1-5]``` | ```1``` | Trust level to apply to newly imported keys or existing keys; please keep in mind that keys with a trust level other than 5 need to be signed by a fully trusted key in order to effectively set the trust level. If your key is not signed by a fully trusted key and the trust level is 2, 3 or 4, the module will report a changed state on each run due to the fact that GnuPG will report an 'Unknown' trust level. | 41 | | **state** | | ```str``` | ```present```/ ```absent```/ ```latest```/ ```info``` | ```present``` | Key should be present, absent, latest (keyserver only) or info. Info only shows info for key given via fpr. Alternatively, you can use the special value * for the fpr to get a list of all installed keys and their relevant info. | 42 | | **gpgbin** | | ```path``` | | ```get_bin_path``` method to find gpg | Full path to GnuPG binary on target host | 43 | | **homedir** | | ```path``` | | ```None``` | Full path to the gpg homedir you wish to use; If none is provided, gpg will use the default homedir of ~/.gnupg Please be aware that this will be the user executing the module on the target host! So there will likely be a difference between running the module with and without become:yes! If you don't want to be surprised, set the path to the homedir with the variable. For more information on the GnuPG homedir, check https://www.gnupg.org/gph/en/manual/r1616.html | 44 | | **keyring** | | ```path``` | | ```None``` | Full Full path to the gpg keyring you wish to use; If none is provided, gpg will use the default. For more information on the GnuPG keyring, check https://www.gnupg.org/gph/en/manual/r1574.html | 45 | 46 | ## Examples 47 | 48 | ```YAML 49 | # install key from keyfile on target host and set trust level to 5 50 | - name: add key(s) from file and set trust 51 | gpg_key: 52 | file: "/tmp/testkey.asc" 53 | trust: '5' 54 | ``` 55 | ```YAML 56 | # make sure all keys in a file are NOT present on the keychain 57 | - name: remove keys inside file from the keychain 58 | gpg_key: 59 | file: "/tmp/testkey.asc" 60 | state: absent 61 | ``` 62 | ```YAML 63 | # install keys on the target host from a keyfile on the ansible master 64 | - name: install keys on the target host from a keyfile on the ansible master 65 | gpg_key: 66 | content: "{{ lookup('file', '/my/tmp/file/on/host') }}" 67 | ``` 68 | ```YAML 69 | # alternatively, you can simply provide the key contents directly 70 | - name: install keys from key contents 71 | content: "-----BEGIN PGP PUBLIC KEY BLOCK-----........." 72 | ``` 73 | ```YAML 74 | # install key from keyserver on target machine 75 | - name: install key from default keyserver on target machine 76 | gpg_key: 77 | fpr: 0D69E11F12BDBA077B3726AB4E1F799AA4FF2279 78 | ``` 79 | ```YAML 80 | # install key from keyserver on target machine and set trust level 81 | - name: install key from alternate keyserver on target machine and set trust level 5 82 | gpg_key: 83 | fpr: 0D69E11F12BDBA077B3726AB4E1F799AA4FF2279 84 | keyserver: eu.pool.sks-keyservers.net 85 | trust: '5' 86 | ``` 87 | ```YAML 88 | # delete a key from the target machine 89 | - name: remove a key from the target machine 90 | gpg_key: 91 | fpr: 0D69E11F12BDBA077B3726AB4E1F799AA4FF2279 92 | state: absent 93 | ``` 94 | ```YAML 95 | # get keyinfo for a specific key; will also return success if key not installed 96 | - name: get keyinfo 97 | gpg_key: 98 | fpr: 0D69E11F12BDBA077B3726AB4E1F799AA4FF2279 99 | state: info 100 | ``` 101 | ```YAML 102 | # get keyinfo for all installed keys, public and secret 103 | - name: get keyinfo for all keys 104 | gpg_key: 105 | fpr: '*' 106 | state: info 107 | ``` 108 | ```YAML 109 | # get keyinfo for a specific key in the keyring /etc/apt/trusted.gpg and save it to info 110 | - name: get keyinfo 111 | gpg_key: 112 | fpr: '15058500A0235D97F5D10063B188E2B695BD4743' 113 | keyring: /etc/apt/trusted.gpg 114 | state: info 115 | register: info 116 | ``` 117 | 118 | ## Return values 119 | 120 | The module returns a dictionary containing 3 main keys: ```fprs```, ```keys``` and ```msg```; a fourth key, ```debug```, is added when the verbosity level of your playbook run is at least 2 (-vv). It contains a bunch of debug statements informing you of the steps the module has taken. 121 | ```fprs``` is a list of unique fingerprints as touched by the module. 122 | ```keys``` contains a list of all keys touched by the module, including any info it could find. 123 | ```msg``` is simply a status message summarizing what the module has done. 124 | 125 | ### Sample output 126 | 127 | ``` 128 | { 129 | 'fprs': 130 | - A0880EC90DD07F5968CEE3B6C6B3D8E7A7CD2528 131 | 'keys': 132 | - A0880EC90DD07F5968CEE3B6C6B3D8E7A7CD2528: 133 | changed: false 134 | creationdate: '1576698396' 135 | curve_name: ed25519 136 | expirationdate: '' 137 | fingerprint: A0880EC90DD07F5968CEE3B6C6B3D8E7A7CD2528 138 | hash_algorithm: '' 139 | key_capabilities: cSC 140 | key_length: '256' 141 | keyid: C6B3D8E7A7CD2528 142 | pubkey_algorithm: Ed25519 143 | state: present 144 | trust_level: u 145 | trust_level_desc: The key is ultimately trusted 146 | type: pub 147 | userid: 'somekey ' 148 | } 149 | ``` 150 | 151 | If you set the state to absent and the key was already absent, obviously not all info will be available; it would look similar to: 152 | ``` 153 | { 154 | 'fprs': 155 | - A0880EC90DD07F5968CEE3B6C6B3D8E7A7CD2528 156 | 'keys': 157 | - A0880EC90DD07F5968CEE3B6C6B3D8E7A7CD2528: 158 | changed: false 159 | fingerprint: A0880EC90DD07F5968CEE3B6C6B3D8E7A7CD2528 160 | state: absent 161 | } 162 | ``` 163 | 164 | ## License & Author 165 | 166 | License: MIT 167 | Written by: Rinck H. Sonnenberg 168 | -------------------------------------------------------------------------------- /gpg_key.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright: (c) 2019, Rinck H. Sonnenberg - Netson 4 | # License: MIT 5 | 6 | ANSIBLE_METADATA = { 7 | 'metadata_version': '1.1', 8 | 'status': ['preview'], 9 | 'supported_by': 'community' 10 | } 11 | 12 | DOCUMENTATION = ''' 13 | --- 14 | module: gpg_key 15 | short_description: Module to install and trust GPG keys 16 | version_added: "2.7" 17 | description: | 18 | Module to install and trust GPG keys from files and keyservers. 19 | I shouldn't have to tell you that it is a BAD idea to store your 20 | secret keys inside a playbook or role! Please take approriate measures 21 | to protect your sensitive information from falling into the wrong hands! 22 | options: 23 | fpr: 24 | description: | 25 | Key Fingerprint to install from keyserver, to delete from target 26 | machine, or to get info on. To get info on all installed keys, 27 | use * as the value for fpr. Using any shorter ID than the full 28 | fingerprint will fail. Using the short ID's isn't recommended 29 | anyways, due to possible collisions. 30 | required: false 31 | type: str 32 | keyserver: 33 | description: Keyserver to download key from 34 | default: keyserver.ubuntu.com 35 | type: str 36 | file: 37 | description: | 38 | File on target machine containing the key(s) to install; 39 | be aware that a file can contain more than 1 key; if this 40 | is the case, all keys will be imported and all keys will 41 | receive the same trust level. The module auto-detects if 42 | the given key is a public or secret key. 43 | required: false 44 | type: path 45 | content: 46 | description: | 47 | Contents of keyfile to install on target machine 48 | just like the file, the contents can contain more than 1 key 49 | and all keys will receive the same trust level. The module 50 | auto-detects if the given key is a public or secret key. 51 | The content parameter simply creates a temporary file on the 52 | target host and then performs the same actions as the file 53 | parameter. It is just an easy method to not have to create 54 | a keyfile on the target machine first. 55 | required: false 56 | type: str 57 | manage_trust: 58 | description: | 59 | Setting controls wether or not the module controls the trust levels 60 | of the (imported) keys. If set to false, no changes will be made to 61 | the trust level regardless of the 'trust' setting. 62 | default: true 63 | type: bool 64 | trust: 65 | description: | 66 | Trust level to apply to newly imported keys or existing keys; 67 | please keep in mind that keys with a trust level other than 5 68 | need to be signed by a fully trusted key in order to effectively 69 | set the trust level. If your key is not signed by a fully trusted 70 | key and the trust level is 2, 3 or 4, the module will report a 71 | changed state on each run due to the fact that GnuPG will report 72 | an 'Unknown' trust level. 73 | choices: 74 | - 1 75 | - 2 76 | - 3 77 | - 4 78 | - 5 79 | default: 1 80 | type: str 81 | state: 82 | description: | 83 | Key should be present, absent, latest (keyserver only) or info. 84 | Info only shows info for key given via fpr. Alternatively, you 85 | can use the special value * for the fpr to get a list of all 86 | installed keys and their relevant info. 87 | default: present 88 | type: str 89 | choices: 90 | - present 91 | - absent 92 | - latest 93 | - info 94 | gpgbin: 95 | description: Full path to GnuPG binary on target host 96 | default: uses get_bin_path method to find gpg 97 | type: path 98 | homedir: 99 | description: | 100 | Full path to the gpg homedir you wish to use; If none is provided, 101 | gpg will use the default homedir of ~/.gnupg 102 | Please be aware that this will be the user executing the module 103 | on the target host! So there will likely be a difference between 104 | running the module with and without become:yes! If you don't want to 105 | be surprised, set the path to the homedir with the variable. For more 106 | information on the GnuPG homedir, check 107 | https://www.gnupg.org/gph/en/manual/r1616.html 108 | default: None 109 | type: path 110 | keyring: 111 | description: | 112 | Full path to the gpg keyring you wish to use; If none is provided, 113 | gpg will use the default 114 | For more information on the GnuPG keyring, check 115 | https://www.gnupg.org/gph/en/manual/r1574.html 116 | default: None 117 | type: path 118 | 119 | author: 120 | - Rinck H. Sonnenberg (r.sonnenberg@netson.nl) 121 | ''' 122 | 123 | EXAMPLES = ''' 124 | # install key from keyfile on target host and set trust level to 5 125 | - name: add key(s) from file and set trust 126 | gpg_key: 127 | file: "/tmp/testkey.asc" 128 | trust: '5' 129 | 130 | # make sure all keys in a file are NOT present on the keychain 131 | - name: remove keys inside file from the keychain 132 | gpg_key: 133 | file: "/tmp/testkey.asc" 134 | state: absent 135 | 136 | # install keys on the target host from a keyfile on the ansible master 137 | - name: install keys on the target host from a keyfile on the ansible master 138 | gpg_key: 139 | content: "{{ lookup('file', '/my/tmp/file/on/host') }}" 140 | 141 | # alternatively, you can simply provide the key contents directly 142 | - name: install keys from key contents 143 | content: "-----BEGIN PGP PUBLIC KEY BLOCK-----........." 144 | 145 | # install key from keyserver on target machine 146 | - name: install key from default keyserver on target machine 147 | gpg_key: 148 | fpr: 0D69E11F12BDBA077B3726AB4E1F799AA4FF2279 149 | 150 | # install key from keyserver on target machine and set trust level 151 | - name: install key from alternate keyserver on target machine and set trust level 5 152 | gpg_key: 153 | fpr: 0D69E11F12BDBA077B3726AB4E1F799AA4FF2279 154 | keyserver: eu.pool.sks-keyservers.net 155 | trust: '5' 156 | 157 | # delete a key from the target machine 158 | - name: remove a key from the target machine 159 | gpg_key: 160 | fpr: 0D69E11F12BDBA077B3726AB4E1F799AA4FF2279 161 | state: absent 162 | 163 | # get keyinfo for a specific key; will also return success if key not installed 164 | - name: get keyinfo 165 | gpg_key: 166 | fpr: 0D69E11F12BDBA077B3726AB4E1F799AA4FF2279 167 | state: info 168 | 169 | # get keyinfo for all installed keys, public and secret 170 | - name: get keyinfo for all keys 171 | gpg_key: 172 | fpr: '*' 173 | state: info 174 | ''' 175 | 176 | RETURN = ''' 177 | keys: 178 | description: | 179 | list of keys touched by the module; 180 | list contains dicts of fingerprint, keytype, capabilities and trust level for each key 181 | an exmaple output would looke like: 182 | A0880EC90DD07F5968CEE3B6C6B3D8E7A7CD2528: 183 | changed: false 184 | creationdate: '1576698396' 185 | curve_name: ed25519 186 | expirationdate: '' 187 | fingerprint: A0880EC90DD07F5968CEE3B6C6B3D8E7A7CD2528 188 | hash_algorithm: '' 189 | key_capabilities: cSC 190 | key_length: '256' 191 | keyid: C6B3D8E7A7CD2528 192 | pubkey_algorithm: Ed25519 193 | state: present 194 | trust_level: u 195 | trust_level_desc: The key is ultimately trusted 196 | type: pub 197 | userid: 'somekey ' 198 | If you set the state to absent, and the key was already absent, obviously 199 | not all info will be available; it would look similar to: 200 | A0880EC90DD07F5968CEE3B6C6B3D8E7A7CD2528: 201 | changed: false 202 | fingerprint: A0880EC90DD07F5968CEE3B6C6B3D8E7A7CD2528 203 | state: absent 204 | type: list 205 | returned: always 206 | debug: 207 | description: contains debug information 208 | type: list 209 | returned: when verbosity >= 2 210 | ''' 211 | 212 | import re 213 | import os 214 | import time 215 | from ansible.module_utils.basic import AnsibleModule 216 | from packaging import version 217 | 218 | # class to import GPG keys 219 | class GpgKey(object): 220 | 221 | 222 | def __init__(self, module): 223 | """ 224 | init method 225 | """ 226 | # set ansible module 227 | self.module = module 228 | self.debugmsg = [] 229 | self.installed_keys = {} 230 | self.changed = False 231 | 232 | # seed the result dict in the object 233 | # we primarily care about changed and state 234 | # change is if this module effectively modified the target 235 | # state will include any data that you want your module to pass back 236 | # for consumption, for example, in a subsequent task 237 | self.result = dict( 238 | changed=False, 239 | keys={}, 240 | msg="", 241 | ) 242 | 243 | # set gpg binary none was provided 244 | if not self.module.params["gpgbin"] or self.module.params["gpgbin"] is None: 245 | self.module.params["gpgbin"] = self.module.get_bin_path('gpg') 246 | 247 | 248 | def _vv(self, msg): 249 | """ 250 | debug info 251 | """ 252 | # add debug message 253 | self.debugmsg.append("{}".format(msg)) 254 | 255 | 256 | def has_method(self, name): 257 | """ 258 | method to check if other methods exist 259 | """ 260 | return callable(getattr(self, name, None)) 261 | 262 | 263 | def run(self): 264 | """ 265 | run module with given parameters 266 | """ 267 | # check versions of gnupg and libgcrypt 268 | # check homedir 269 | self.check_versions() 270 | self.check_homedir() 271 | 272 | # determine and run action 273 | if self.module.params["file"]: 274 | run_action = "file" 275 | elif self.module.params["fpr"]: 276 | run_action = "fpr" 277 | elif self.module.params["content"]: 278 | run_action = "content" 279 | else: 280 | self.module.fail_json(msg="You shouldn't be here; no valid action could be determined") 281 | 282 | # determine action and method 283 | run_state = self.module.params["state"] 284 | run_method = "run_{}_{}".format(run_action, run_state) 285 | self._vv("determined action [{}] with state [{}]".format(run_action, run_state)) 286 | 287 | # always check installed keys first 288 | self.check_installed_keys() 289 | #self.result["installed_keys"] = self.installed_keys 290 | 291 | # check if run method exists, and if not fail with an error 292 | if self.has_method(run_method): 293 | getattr(self, run_method)() 294 | else: 295 | self.module.fail_json(msg="Action [{}] is not supported with state [{}]".format(run_action, run_state)) 296 | 297 | # check verbosity and add debug messages 298 | if self.module._verbosity >= 2: 299 | self.result['debug'] = "\n".join(self.debugmsg) 300 | 301 | # return result 302 | return self.result 303 | 304 | 305 | def run_file_present(self): 306 | """ 307 | import key from file 308 | """ 309 | # first, check if the file is OK 310 | keyinfo = self.check_file() 311 | 312 | self._vv("import new keys from file") 313 | 314 | # import count 315 | impcnt = 0 316 | trucnt = 0 317 | 318 | # then see if the key is already installed 319 | # fk = file key 320 | # ik = installed key 321 | for index, fk in enumerate(keyinfo["keys"]): 322 | 323 | # check expiration by checking trust 324 | if fk["trust_level"] in ['i','d','r','e']: 325 | self.module.fail_json(msg="key is either expired or invalid [trust={}] [expiration={}]".format(fk["trust_level"], fk["expirationdate"])) 326 | 327 | # check if key is installed 328 | installed = False 329 | for ik in self.installed_keys["keys"]: 330 | if (fk["fingerprint"].upper() == ik["fingerprint"].upper() and 331 | fk["type"] == ik["type"] and 332 | fk["key_capabilities"] == ik["key_capabilities"] 333 | ): 334 | self._vv("fingerprint [{}] already installed".format(fk["fingerprint"])) 335 | keyinfo["keys"][index]["state"] = "present" 336 | keyinfo["keys"][index]["changed"] = False 337 | installed = True 338 | 339 | # check trust 340 | if not self.compare_trust(fk["trust_level"], self.module.params["trust"]): 341 | 342 | # update trust level 343 | self.set_trust(fk["fingerprint"], self.module.params["trust"]) 344 | trucnt += 1 345 | 346 | # get trust level as displayed by gpg 347 | tru_level, tru_desc = self.get_trust(self.module.params["trust"]) 348 | keyinfo["keys"][index]["changed"] = True 349 | keyinfo["keys"][index]["trust_level"] = tru_level 350 | keyinfo["keys"][index]["trust_level_desc"] = tru_desc 351 | 352 | continue 353 | 354 | if not installed: 355 | 356 | self._vv("fingerprint [{}] not yet installed".format(fk["fingerprint"])) 357 | 358 | # import file 359 | cmd = self.prepare_command("file", "present") 360 | 361 | # run subprocess 362 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 363 | self._vv("fingerprint [{}] successfully imported".format(fk["fingerprint"])) 364 | keyinfo["keys"][index]["state"] = "present" 365 | keyinfo["keys"][index]["changed"] = True 366 | impcnt += 1 367 | 368 | # check trust 369 | if not self.compare_trust(fk["trust_level"], self.module.params["trust"]): 370 | 371 | # update trust level 372 | self.set_trust(fk["fingerprint"], self.module.params["trust"]) 373 | trucnt += 1 374 | 375 | # get trust level as displayed by gpg 376 | tru_level, tru_desc = self.get_trust(self.module.params["trust"]) 377 | keyinfo["keys"][index]["changed"] = True 378 | keyinfo["keys"][index]["trust_level"] = tru_level 379 | keyinfo["keys"][index]["trust_level_desc"] = tru_desc 380 | 381 | # set keyinfo 382 | self.set_keyinfo(keyinfo) 383 | 384 | # check import count 385 | if impcnt > 0 or trucnt > 0: 386 | self.result["changed"] = True 387 | 388 | # set message and return 389 | self.result["msg"] = "[{}] keys were imported; [{}] trust levels updated".format(impcnt, trucnt) 390 | return True 391 | 392 | 393 | def run_file_absent(self): 394 | """ 395 | remove key(s) present in file 396 | """ 397 | # first, check if the file is OK 398 | keyinfo = self.check_file() 399 | 400 | self._vv("remove keys identified in file") 401 | 402 | # key count 403 | keycnt = 0 404 | 405 | # then see if the key is installed or not 406 | # fk = file key 407 | # ik = installed key 408 | for index, fk in enumerate(keyinfo["keys"]): 409 | installed = False 410 | for ik in self.installed_keys["keys"]: 411 | if (fk["fingerprint"].upper() == ik["fingerprint"].upper() and 412 | fk["type"] == ik["type"] and 413 | fk["key_capabilities"] == ik["key_capabilities"] 414 | ): 415 | installed = True 416 | continue 417 | 418 | if not installed: 419 | self._vv("fingerprint [{}] not installed; nothing to remove".format(fk["fingerprint"])) 420 | keyinfo["keys"][index]["state"] = "absent" 421 | keyinfo["keys"][index]["changed"] = False 422 | 423 | else: 424 | 425 | self._vv("fingerprint [{}] installed; will be removed".format(fk["fingerprint"])) 426 | 427 | # remove file 428 | cmd = self.prepare_command("file", "absent") 429 | 430 | # add fingerprint as argument 431 | cmd += [fk["fingerprint"]] 432 | 433 | # run subprocess 434 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 435 | self._vv("fingerprint [{}] successfully removed".format(fk["fingerprint"])) 436 | keyinfo["keys"][index]["state"] = "absent" 437 | keyinfo["keys"][index]["changed"] = True 438 | keycnt += 1 439 | 440 | # re-run check installed command to prevent attempting to remove same 441 | # fingerprint again (for example after removing pub/sec counterpart 442 | # with the same fpr 443 | self.check_installed_keys() 444 | 445 | # set keyinfo 446 | self.set_keyinfo(keyinfo) 447 | 448 | # check import count 449 | if keycnt > 0: 450 | self.result["changed"] = True 451 | 452 | # return 453 | self.result["msg"] = "[{}] keys were removed".format(keycnt) 454 | return True 455 | 456 | 457 | def run_file_info(self): 458 | """ 459 | method to only retrive current status of keys 460 | wether from file, content or fpr 461 | """ 462 | # first, check if the file is OK 463 | keyinfo = self.check_file() 464 | 465 | self._vv("showing key info from file") 466 | 467 | # then see if the key is already installed 468 | # fk = file key 469 | # ik = installed key 470 | for index, fk in enumerate(keyinfo["keys"]): 471 | 472 | # check if key is installed 473 | installed = False 474 | for ik in self.installed_keys["keys"]: 475 | if (fk["fingerprint"].upper() == ik["fingerprint"].upper() and 476 | fk["type"] == ik["type"] and 477 | fk["key_capabilities"] == ik["key_capabilities"] 478 | ): 479 | self._vv("fingerprint [{}] installed".format(fk["fingerprint"])) 480 | keyinfo["keys"][index]["state"] = "present" 481 | keyinfo["keys"][index]["changed"] = False 482 | installed = True 483 | continue 484 | 485 | if not installed: 486 | # set state 487 | self._vv("fingerprint [{}] not installed".format(fk["fingerprint"])) 488 | keyinfo["keys"][index]["state"] = "absent" 489 | keyinfo["keys"][index]["changed"] = False 490 | 491 | # set keyinfo 492 | self.set_keyinfo(keyinfo) 493 | 494 | # set message and return 495 | return True 496 | 497 | 498 | def run_content_present(self): 499 | """ 500 | import keys from content 501 | """ 502 | # prepare content 503 | filename = self.prepare_content(self.module.params["content"]) 504 | 505 | # set file parameter and run file present 506 | self.module.params["file"] = filename 507 | self.run_file_present() 508 | 509 | # delete content 510 | self.delete_content(filename) 511 | 512 | 513 | def run_content_absent(self): 514 | """ 515 | remove keys from content 516 | """ 517 | # prepare content 518 | filename = self.prepare_content(self.module.params["content"]) 519 | 520 | # set file parameter and run file present 521 | self.module.params["file"] = filename 522 | self.run_file_absent() 523 | 524 | # delete content 525 | self.delete_content(filename) 526 | 527 | 528 | def run_content_info(self): 529 | """ 530 | get key info from content 531 | """ 532 | # prepare content 533 | filename = self.prepare_content(self.module.params["content"]) 534 | 535 | # set file parameter and run file present 536 | self.module.params["file"] = filename 537 | self.run_file_info() 538 | 539 | # delete content 540 | self.delete_content(filename) 541 | 542 | 543 | def run_fpr_present(self): 544 | """ 545 | import key from keyserver 546 | """ 547 | self._vv("import new keys from keyserver") 548 | 549 | # set fpr shorthand 550 | fpr = self.module.params["fpr"] 551 | 552 | # set base values 553 | installed = False 554 | impcnt = 0 555 | trucnt = 0 556 | keyinfo = { 557 | 'fprs': [], 558 | 'keys': [], 559 | } 560 | 561 | # check if key is installed 562 | for ik in self.installed_keys["keys"]: 563 | 564 | if (fpr.upper() == ik["fingerprint"].upper()): 565 | 566 | # set keyinfo 567 | self._vv("fingerprint [{}] already installed".format(fpr)) 568 | keyinfo["fprs"].append(fpr) 569 | keyinfo["keys"].append(ik) 570 | keyinfo["keys"][0]["state"] = "present" 571 | keyinfo["keys"][0]["changed"] = False 572 | installed = True 573 | 574 | # check trust 575 | if not self.compare_trust(ik["trust_level"], self.module.params["trust"]): 576 | 577 | # update trust level 578 | self.set_trust(fpr, self.module.params["trust"]) 579 | trucnt += 1 580 | 581 | # get trust level as displayed by gpg 582 | tru_level, tru_desc = self.get_trust(self.module.params["trust"]) 583 | keyinfo["keys"][0]["changed"] = True 584 | keyinfo["keys"][0]["trust_level"] = tru_level 585 | keyinfo["keys"][0]["trust_level_desc"] = tru_desc 586 | 587 | continue 588 | 589 | if not installed: 590 | 591 | self._vv("fingerprint [{}] not yet installed".format(fpr)) 592 | 593 | # import file 594 | cmd = self.prepare_command("fpr", "present") 595 | cmd += [fpr] 596 | 597 | # run subprocess 598 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 599 | self._vv("fingerprint [{}] successfully imported from keyserver".format(fpr)) 600 | 601 | # get info from specific key; keyservers only contain public keys 602 | # so no point in checking the secret keys 603 | cmd = self.prepare_command("check", "installed_public") 604 | cmd += [fpr] 605 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 606 | keyinfo = self.process_colons(stdout) 607 | 608 | # check expiration by checking trust 609 | if keyinfo["keys"][0]["trust_level"] in ['i','d','r','e']: 610 | # deleted the expired key and fail 611 | cmd = self.prepare_command("fpr", "absent") 612 | cmd += [fpr] 613 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 614 | self.module.fail_json(msg="key is either expired or invalid [trust={}] [expiration={}]".format(keyinfo["keys"][0]["trust_level"], keyinfo["keys"][0]["expirationdate"])) 615 | 616 | # update key info 617 | keyinfo["keys"][0]["state"] = "present" 618 | keyinfo["keys"][0]["changed"] = True 619 | impcnt += 1 620 | 621 | # check trust 622 | if not self.compare_trust(keyinfo["keys"][0]["trust_level"], self.module.params["trust"]): 623 | 624 | # update trust level 625 | self.set_trust(fpr, self.module.params["trust"]) 626 | trucnt += 1 627 | 628 | # get trust level as displayed by gpg 629 | tru_level, tru_desc = self.get_trust(self.module.params["trust"]) 630 | keyinfo["keys"][0]["changed"] = True 631 | keyinfo["keys"][0]["trust_level"] = tru_level 632 | keyinfo["keys"][0]["trust_level_desc"] = tru_desc 633 | 634 | # set keyinfo 635 | self.set_keyinfo(keyinfo) 636 | 637 | # check import count 638 | if impcnt > 0 or trucnt > 0: 639 | self.result["changed"] = True 640 | 641 | # set message and return 642 | self.result["msg"] = "[{}] keys were imported; [{}] trust levels updated".format(impcnt, trucnt) 643 | return True 644 | 645 | 646 | def run_fpr_absent(self): 647 | """ 648 | remove key(s) 649 | """ 650 | self._vv("delete keys based on fingerprint") 651 | 652 | # set fpr shorthand 653 | fpr = self.module.params["fpr"] 654 | 655 | # set base values 656 | installed = False 657 | keycnt = 0 658 | keyinfo = { 659 | 'fprs': [], 660 | 'keys': [], 661 | } 662 | 663 | # see if the key is installed or not 664 | # ik = installed key 665 | for ik in self.installed_keys["keys"]: 666 | if fpr.upper() == ik["fingerprint"].upper(): 667 | if ("state" in ik and ik["state"] != "absent") or ("state" not in ik): 668 | keyinfo["fprs"].append(fpr) 669 | keyinfo["keys"].append(ik) 670 | installed = True 671 | continue 672 | 673 | if not installed: 674 | 675 | self._vv("fingerprint [{}] not installed; nothing to remove".format(fpr)) 676 | key = {} 677 | key[fpr] = { 678 | "state" : "absent", 679 | "changed" : False, 680 | "fingerprint" : fpr, 681 | } 682 | keyinfo["fprs"].append(fpr) 683 | keyinfo["keys"].append(key) 684 | 685 | else: 686 | 687 | self._vv("fingerprint [{}] installed; will be removed".format(fpr)) 688 | 689 | # remove file 690 | cmd = self.prepare_command("fpr", "absent") 691 | 692 | # add fingerprint as argument 693 | cmd += [fpr] 694 | 695 | # run subprocess 696 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 697 | self._vv("fingerprint [{}] successfully removed".format(fpr)) 698 | keyinfo["keys"][0]["state"] = "absent" 699 | keyinfo["keys"][0]["changed"] = True 700 | keycnt += 1 701 | 702 | # re-run check installed command to prevent attempting to remove same 703 | # fingerprint again (for example after removing pub/sec counterpart 704 | # with the same fpr 705 | self.check_installed_keys() 706 | 707 | # set keyinfo 708 | self.set_keyinfo(keyinfo) 709 | 710 | # check import count 711 | if keycnt > 0: 712 | self.result["changed"] = True 713 | 714 | # return 715 | self.result["msg"] = "[{}] keys were removed".format(keycnt) 716 | return True 717 | 718 | 719 | def run_fpr_latest(self): 720 | """ 721 | get the latest key from the keyserver 722 | """ 723 | self._vv("get latest key from keyserver") 724 | 725 | # set fpr shorthand 726 | fpr = self.module.params["fpr"] 727 | 728 | # set base values 729 | installed = False 730 | updated = False 731 | updcnt = 0 732 | trucnt = 0 733 | keyinfo = { 734 | 'fprs': [], 735 | 'keys': [], 736 | } 737 | 738 | # check if key is installed 739 | for ik in self.installed_keys["keys"]: 740 | 741 | if (fpr == ik["fingerprint"]): 742 | 743 | # set keyinfo 744 | self._vv("fingerprint [{}] installed; updating from server".format(fpr)) 745 | keyinfo["fprs"].append(fpr) 746 | keyinfo["keys"].append(ik) 747 | keyinfo["keys"][0]["state"] = "present" 748 | keyinfo["keys"][0]["changed"] = False 749 | installed = True 750 | continue 751 | 752 | if not installed: 753 | 754 | self._vv("fingerprint [{}] not yet installed; install first".format(fpr)) 755 | 756 | # import from keyserver 757 | self.run_fpr_present() 758 | return True 759 | 760 | else: 761 | 762 | self._vv("fetching updates from keyserver") 763 | 764 | # get updates from keyserver 765 | cmd = self.prepare_command("fpr", "latest") 766 | cmd += [fpr] 767 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 768 | 769 | # see if any updates were downloaded or not 770 | # for some reason, gpg outputs these messages to stderr 771 | updated = re.search(r'gpg:\s+unchanged: 1\n', stderr) is None 772 | if updated: 773 | updcnt += 1 774 | 775 | # if key was updated, refresh info 776 | if updated: 777 | 778 | self._vv("key was updated on server") 779 | 780 | # get info from specific key; keyservers only contain public keys 781 | # so no point in checking the secret keys 782 | cmd = self.prepare_command("check", "installed_public") 783 | cmd += [fpr] 784 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 785 | keyinfo = self.process_colons(stdout) 786 | 787 | # check expiration by checking trust 788 | if keyinfo["keys"][0]["trust_level"] in ['i','d','r','e']: 789 | # deleted the expired key and fail 790 | cmd = self.prepare_command("fpr", "absent") 791 | cmd += [fpr] 792 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 793 | self.module.fail_json(msg="key is either expired or invalid [trust={}] [expiration={}]".format(keyinfo["keys"][0]["trust_level"], keyinfo["keys"][0]["expirationdate"])) 794 | 795 | # update key info 796 | keyinfo["keys"][0]["state"] = "present" 797 | keyinfo["keys"][0]["changed"] = True 798 | 799 | # check trust 800 | if not self.compare_trust(keyinfo["keys"][0]["trust_level"], self.module.params["trust"]): 801 | 802 | # update trust level 803 | self.set_trust(fpr, self.module.params["trust"]) 804 | 805 | # get trust level as displayed by gpg 806 | tru_level, tru_desc = self.get_trust(self.module.params["trust"]) 807 | keyinfo["keys"][0]["changed"] = True 808 | keyinfo["keys"][0]["trust_level"] = tru_level 809 | keyinfo["keys"][0]["trust_level_desc"] = tru_desc 810 | trucnt += 1 811 | 812 | # set keyinfo 813 | self.set_keyinfo(keyinfo) 814 | 815 | # check import count 816 | if updcnt > 0 or trucnt > 0: 817 | self.result["changed"] = True 818 | 819 | # set message and return 820 | self.result["msg"] = "[{}] keys were updated; [{}] trust levels updated".format(updcnt, trucnt) 821 | return True 822 | 823 | 824 | def run_fpr_info(self): 825 | """ 826 | method to only return current key info 827 | will never report changed as it doesn't change anything on the target 828 | """ 829 | # frp shorthand 830 | fpr = self.module.params["fpr"] 831 | keycount = 0 832 | 833 | # check if the request is for a single key or all 834 | if fpr == "*": 835 | keyinfo = self.installed_keys 836 | keycount = len(self.installed_keys["keys"]) 837 | 838 | else: 839 | # then see if the key is already installed 840 | # ik = installed key 841 | installed = False 842 | keycount = 1 843 | keyinfo = { 844 | "fprs": [], 845 | "keys": [{}], # Initialize keys list with an empty dictionary 846 | } 847 | 848 | for ik in self.installed_keys["keys"]: 849 | if fpr.upper() == ik["fingerprint"].upper(): 850 | self._vv("Fingerprint [{}] installed".format(fpr)) 851 | keyinfo["fprs"].append(fpr) 852 | keyinfo["keys"].append(ik) 853 | keyinfo["keys"][0]["state"] = "present" 854 | keyinfo["keys"][0]["changed"] = False 855 | installed = True 856 | break 857 | 858 | if not installed: 859 | # set state 860 | self._vv("fingerprint [{}] not installed".format(fpr)) 861 | keyinfo["fprs"].append(fpr) 862 | keyinfo["keys"].append({}) 863 | keyinfo["keys"][0]["fingerprint"] = fpr 864 | keyinfo["keys"][0]["state"] = "absent" 865 | keyinfo["keys"][0]["changed"] = False 866 | 867 | # set keyinfo 868 | self.set_keyinfo(keyinfo) 869 | self.result["msg"] = "listing info for [{}] key(s)".format(keycount) 870 | 871 | 872 | def prepare_content(self, content): 873 | """ 874 | prepare content 875 | """ 876 | # create temporary file and write contents 877 | filename = "tmp-gpg-{}.asc".format(time.time()) 878 | self._vv("writing content to temporary file [{}]".format(filename)) 879 | tmpfile = open("{}".format(filename),"w+") 880 | tmpfile.write(content) 881 | tmpfile.close() 882 | 883 | # return filename 884 | return filename 885 | 886 | 887 | def delete_content(self, filename): 888 | """ 889 | delete temporary content 890 | """ 891 | # cleanup 892 | self._vv("deleting temporary file [{}]".format(filename)) 893 | os.remove(filename) 894 | 895 | 896 | def prepare_command(self, action, state): 897 | """ 898 | prepare any gpg command 899 | """ 900 | # set base command 901 | cmd = [self.module.params["gpgbin"]] 902 | 903 | # determine dry run / check mode 904 | if self.module.check_mode: 905 | cmd.append("--dry-run") 906 | 907 | # determine if homedir was set 908 | if self.module.params["homedir"]: 909 | cmd.append("--homedir") 910 | cmd.append(self.module.params["homedir"]) 911 | 912 | # determine if keyring was set 913 | if self.module.params["keyring"]: 914 | cmd.append("--keyring") 915 | cmd.append(self.module.params["keyring"]) 916 | 917 | # check versions 918 | if action == "check" and state == "versions": 919 | args = ["--version"] 920 | 921 | # check installed public keys 922 | if action == "check" and state == "installed_public": 923 | args = [ 924 | "--with-colons", 925 | "--list-keys", 926 | ] 927 | 928 | # check installed secret keys 929 | if action == "check" and state == "installed_secret": 930 | args = [ 931 | "--with-colons", 932 | "--list-secret-keys", 933 | ] 934 | 935 | # check file 936 | if action == "check" and state == "file": 937 | args = [ 938 | "--with-colons", 939 | "--dry-run", 940 | "--import-options", 941 | "import-show", 942 | "--import", 943 | self.module.params["file"], 944 | ] 945 | 946 | # file present 947 | if action == "file" and state == "present": 948 | args = [ 949 | "--batch", 950 | "--import", 951 | self.module.params["file"], 952 | ] 953 | 954 | # file absent 955 | if action == "file" and state == "absent": 956 | args = [ 957 | "--batch", 958 | "--yes", 959 | "--delete-secret-and-public-key", 960 | ] 961 | 962 | # set ownertrust 963 | if action == "set" and state == "trust": 964 | args = ["--import-ownertrust"] 965 | 966 | # fpr present 967 | if action == "fpr" and state == "present": 968 | args = ["--recv-keys"] 969 | 970 | # determine if keyserver 971 | if self.module.params["keyserver"]: 972 | cmd.append("--keyserver") 973 | cmd.append(self.module.params["keyserver"]) 974 | 975 | # fpr absent 976 | if action == "fpr" and state == "absent": 977 | args = [ 978 | "--batch", 979 | "--yes", 980 | "--delete-secret-and-public-key", 981 | ] 982 | 983 | # fpr latest 984 | if action == "fpr" and state == "latest": 985 | args = ["--refresh-keys"] 986 | 987 | # determine if keyserver 988 | if self.module.params["keyserver"]: 989 | cmd.append("--keyserver") 990 | cmd.append(self.module.params["keyserver"]) 991 | 992 | # merge cmd and args and return 993 | cmd += args 994 | self._vv("running command [{}]".format(" ".join(cmd))) 995 | return cmd 996 | 997 | 998 | def check_versions(self): 999 | """ 1000 | function to verify we have the right gnupg2 version 1001 | """ 1002 | self._vv("checking gnupg and libgcrypt versions") 1003 | 1004 | # set command 1005 | cmd = self.prepare_command("check", "versions") 1006 | 1007 | # run subprocess 1008 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 1009 | 1010 | # stdout lines - run_command returns a single string and we need the first and second line only 1011 | lines = stdout.splitlines() 1012 | 1013 | # find gpg version 1014 | regex_gpg = r"gpg\s+\(GnuPG[^)]*\)\s+(\d+\.\d+\.?\d*)$" 1015 | match_gpg = re.search(regex_gpg, lines[0]) 1016 | 1017 | # sanity check 1018 | if match_gpg is None or match_gpg.group(1) is None: 1019 | self.module.fail_json(msg="could not find a valid gpg version number in string [{}]".format(lines[0])) 1020 | 1021 | # find libgcrypt version 1022 | regex_libgcrypt = r"libgcrypt\s+(\d+\.\d+\.?\d*)" 1023 | match_libgcrypt = re.match(regex_libgcrypt, lines[1]) 1024 | 1025 | # sanity check 1026 | if match_libgcrypt is None or match_libgcrypt.group(1) is None: 1027 | self.module.fail_json(msg="could not find a valid libgcrypt version number in string [{}]".format(lines[1])) 1028 | 1029 | # check versions 1030 | versions = {'gpg' : match_gpg.group(1), 1031 | 'libgcrypt' : match_libgcrypt.group(1), 1032 | } 1033 | req_gpg = '2.1.17' 1034 | req_libgcrypt = '1.8.1' 1035 | 1036 | # display minimum versions 1037 | self._vv("gpg_key module requires at least gnupg version [{}] and libgcrypt version [{}]".format(versions['gpg'], versions['libgcrypt'])) 1038 | 1039 | # sanity check 1040 | if version.parse(versions['gpg']) < version.parse(req_gpg) or version.parse(versions['libgcrypt']) < version.parse(req_libgcrypt): 1041 | self.module.fail_json(msg="gpg version [{}] and libgcrypt version [{}] are required; [{}] and [{}] given".format(req_gpg, req_libgcrypt, versions['gpg'], versions['libgcrypt'])) 1042 | else: 1043 | self._vv("gnupg version [{}] and libgcrypt version [{}] detected".format(versions['gpg'], versions['libgcrypt'])) 1044 | 1045 | return True 1046 | 1047 | 1048 | def check_installed_keys(self): 1049 | """ 1050 | get list of keyfiles from current gpg homedir 1051 | """ 1052 | self._vv("checking installed public keys on target host") 1053 | 1054 | # set command 1055 | cmd = self.prepare_command("check", "installed_public") 1056 | 1057 | # run subprocess 1058 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 1059 | 1060 | # get public key info 1061 | pubkeyinfo = self.process_colons(stdout) 1062 | 1063 | self._vv("found a total of [{}] public keys on target host".format(len(pubkeyinfo["fprs"]))) 1064 | 1065 | self._vv("checking installed secret keys on target host") 1066 | 1067 | # set command 1068 | cmd = self.prepare_command("check", "installed_secret") 1069 | 1070 | # run subprocess 1071 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 1072 | 1073 | # get public key info 1074 | seckeyinfo = self.process_colons(stdout) 1075 | 1076 | self._vv("found a total of [{}] secret keys on target host".format(len(seckeyinfo["fprs"]))) 1077 | 1078 | # merge keys 1079 | keyinfo = { 1080 | 'fprs': pubkeyinfo["fprs"]+seckeyinfo["fprs"], 1081 | 'keys': pubkeyinfo["keys"]+seckeyinfo["keys"], 1082 | } 1083 | 1084 | # remove any duplicate fingerprints which may occur in both pub and sec keys 1085 | keyinfo["fprs"] = list(dict.fromkeys(keyinfo["fprs"])) 1086 | 1087 | # set keyinfo 1088 | self.installed_keys = keyinfo 1089 | 1090 | 1091 | def check_homedir(self): 1092 | """ 1093 | check homedir 1094 | """ 1095 | # check if homedir exists, if not, fail 1096 | if self.module.params["homedir"] and not os.path.isdir(self.module.params["homedir"]): 1097 | self.module.fail_json(msg="given homedir [{}] does not exist or not accessible by current ansible user".format(self.module.params["homedir"])) 1098 | 1099 | self._vv("homedir set to [{}]".format(self.module.params["homedir"])) 1100 | 1101 | return True 1102 | 1103 | 1104 | def check_file(self): 1105 | """ 1106 | check if param file exists on target machine 1107 | check if file is a valid keyfile 1108 | check for fingerprints 1109 | """ 1110 | self._vv("checking keyfile on target host") 1111 | 1112 | # sanity check 1113 | if not os.path.isfile(self.module.params["file"]): 1114 | self.module.fail_json(msg="the keyfile [{}] does not exist on the target machine".format(self.module.params["file"])) 1115 | else: 1116 | self._vv("keyfile [{}] exists on target host".format(self.module.params["file"])) 1117 | 1118 | # get key info from file 1119 | cmd = self.prepare_command("check", "file") 1120 | 1121 | # run subprocess 1122 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 1123 | keyinfo = self.process_colons(stdout) 1124 | 1125 | return keyinfo 1126 | 1127 | 1128 | def process_colons(self, cinfo): 1129 | """ 1130 | fetch key information from colon output 1131 | """ 1132 | # 1133 | # SAMPLE DATA 1134 | # 1135 | # sec:u:256:22:41343326127FD34F:1566067845:::u:::cC:::+::ed25519:::0: 1136 | # fpr:::::::::0D18E4B6B2698560729D00CE41343326127FD34F: 1137 | # grp:::::::::54AA357FD85BA4D4B7CE86016A3734F00B1BDD07: 1138 | # uid:u::::1566067845::00B9F0DC33EE293CC1E687FFA54A5EA805FD78F8::testing145 (TESTINGCOMM) ::::::::::0: 1139 | # 1140 | 1141 | # 1142 | # line types 1143 | # 1144 | # *** Field 1 - Type of record 1145 | # 1146 | # - pub :: Public key 1147 | # - crt :: X.509 certificate 1148 | # - crs :: X.509 certificate and private key available 1149 | # - sub :: Subkey (secondary key) 1150 | # - sec :: Secret key 1151 | # - ssb :: Secret subkey (secondary key) 1152 | # - uid :: User id 1153 | # - uat :: User attribute (same as user id except for field 10). 1154 | # - sig :: Signature 1155 | # - rev :: Revocation signature 1156 | # - rvs :: Revocation signature (standalone) [since 2.2.9] 1157 | # - fpr :: Fingerprint (fingerprint is in field 10) 1158 | # - pkd :: Public key data [*] 1159 | # - grp :: Keygrip 1160 | # - rvk :: Revocation key 1161 | # - tfs :: TOFU statistics [*] 1162 | # - tru :: Trust database information [*] 1163 | # - spk :: Signature subpacket [*] 1164 | # - cfg :: Configuration data [*] 1165 | # 1166 | # Records marked with an asterisk are described at [[*Special%20field%20formats][*Special fields]]. 1167 | # 1168 | 1169 | # 1170 | # *** Field 12 - Key capabilities 1171 | # 1172 | # The defined capabilities are: 1173 | # 1174 | # - e :: Encrypt 1175 | # - s :: Sign 1176 | # - c :: Certify 1177 | # - a :: Authentication 1178 | # - ? :: Unknown capability 1179 | # 1180 | # A key may have any combination of them in any order. In addition 1181 | # to these letters, the primary key has uppercase versions of the 1182 | # letters to denote the _usable_ capabilities of the entire key, and 1183 | # a potential letter 'D' to indicate a disabled key. 1184 | # 1185 | 1186 | # 1187 | # FIELD TYPES: 1188 | # 1189 | # - Field 1 - Type of record 1190 | # - Field 2 - Validity 1191 | # - Field 3 - Key length 1192 | # - Field 4 - Public key algorithm 1193 | # - Field 5 - KeyID 1194 | # - Field 6 - Creation date 1195 | # - Field 7 - Expiration date 1196 | # - Field 8 - Certificate S/N, UID hash, trust signature info 1197 | # - Field 9 - Ownertrust 1198 | # - Field 10 - User-ID 1199 | # - Field 11 - Signature class 1200 | # - Field 12 - Key capabilities 1201 | # - Field 13 - Issuer certificate fingerprint or other info 1202 | # - Field 14 - Flag field 1203 | # - Field 15 - S/N of a token 1204 | # - Field 16 - Hash algorithm 1205 | # - Field 17 - Curve name 1206 | # - Field 18 - Compliance flags 1207 | # - Field 19 - Last update 1208 | # - Field 20 - Origin 1209 | # - Field 21 - Comment 1210 | # 1211 | 1212 | # determine the correct line 1213 | main_lines = ['sec','ssb','pub','sub'] 1214 | follow_lines = ['fpr','grp','uid'] 1215 | 1216 | # indexes start at 0 1217 | # main parts are for main_lines only 1218 | mainparts = { 1219 | 'type' : 0, 1220 | 'trust_level' : 1, 1221 | 'key_length' : 2, 1222 | 'pubkey_algorithm' : 3, 1223 | 'keyid' : 4, 1224 | 'creationdate' : 5, 1225 | 'expirationdate' : 6, 1226 | 'key_capabilities' : 11, 1227 | 'hash_algorithm' : 15, 1228 | 'curve_name' : 16, 1229 | } 1230 | 1231 | # indexes start at 0 1232 | # follow parts for follow_lines only 1233 | followparts = { 1234 | 'type' : 0, 1235 | 'userid' : 9, # this is the fingerprint for fpr records and the keygrip for grp records 1236 | } 1237 | 1238 | # 1239 | # 9.1. Public-Key Algorithms 1240 | # 1241 | # ID Algorithm 1242 | # -- --------- 1243 | # 1 - RSA (Encrypt or Sign) [HAC] 1244 | # 2 - RSA Encrypt-Only [HAC] 1245 | # 3 - RSA Sign-Only [HAC] 1246 | # 16 - Elgamal (Encrypt-Only) [ELGAMAL] [HAC] 1247 | # 17 - DSA (Digital Signature Algorithm) [FIPS186] [HAC] 1248 | # 18 - Reserved for Elliptic Curve 1249 | # 19 - Reserved for ECDSA 1250 | # 20 - Reserved (formerly Elgamal Encrypt or Sign) 1251 | # 21 - Reserved for Diffie-Hellman (X9.42, 1252 | # as defined for IETF-S/MIME) 1253 | # 22 - Ed25519 1254 | # 100 to 110 - Private/Experimental algorithm 1255 | # 1256 | pubkeys = { 1257 | '1' : 'RSA (Encrypt or Sign)', 1258 | '2' : 'RSA Encrypt-Only', 1259 | '3' : 'RSA Sign-Only', 1260 | '16' : 'Elgamal (Encrypt-Only)', 1261 | '17' : 'DSA [FIPS186]', 1262 | '18' : 'Cv25519', 1263 | '22' : 'Ed25519', 1264 | } 1265 | 1266 | # 1267 | # 2. Field: A letter describing the calculated trust. This is a single 1268 | # letter, but be prepared that additional information may follow 1269 | # in some future versions. (not used for secret keys) 1270 | # 1271 | # o = Unknown (this key is new to the system) 1272 | # i = The key is invalid (e.g. due to a missing self-signature) 1273 | # d = The key has been disabled 1274 | # r = The key has been revoked 1275 | # e = The key has expired 1276 | # - = Unknown trust (i.e. no value assigned) 1277 | # q = Undefined trust; '-' and 'q' may safely be treated as the same value for most purposes 1278 | # n = Don't trust this key at all 1279 | # m = There is marginal trust in this key 1280 | # f = The key is full trusted. 1281 | # u = The key is ultimately trusted; this is only used for 1282 | # keys for which the secret key is also available. 1283 | # 1284 | trustlevels = { 1285 | 'o' : 'Unknown/new', 1286 | 'i' : 'The key is invalid', 1287 | 'd' : 'The key has been disabled', 1288 | 'r' : 'The key has been revoked', 1289 | 'e' : 'The key has expired', 1290 | '-' : 'Unknown trust', 1291 | 'q' : 'Undefined trust', 1292 | 'n' : 'Dont trust this key at all', 1293 | 'm' : 'There is marginal trust in this key', 1294 | 'f' : 'The key is fully trusted', 1295 | 'u' : 'The key is ultimately trusted', 1296 | } 1297 | 1298 | # set list of keys and list of fingerprints 1299 | keys = [] 1300 | fprs = [] 1301 | 1302 | # set empty key dict 1303 | curKey = {} 1304 | 1305 | # loop through lines 1306 | for l in cinfo.splitlines(): 1307 | 1308 | # split line into pieces 1309 | pieces = l.split(":") 1310 | 1311 | # get current line type 1312 | curType = pieces[mainparts.get('type')] 1313 | 1314 | # check for usage/capabilities 1315 | if curType in main_lines: 1316 | 1317 | # check if curKey has values; if so add them to keys list first 1318 | if "type" in curKey: 1319 | self._vv("found [{}] key with fingerprint [{}]".format(curKey["type"], curKey["fingerprint"])) 1320 | keys.append(curKey) 1321 | 1322 | # get pubkey algorithm 1323 | p = pieces[mainparts.get('pubkey_algorithm')] 1324 | z = pubkeys.get(p) if p is not None else '' 1325 | 1326 | # get trustlevel description 1327 | p = pieces[mainparts.get('trust_level')] 1328 | t = trustlevels.get(p) if p is not None else '' 1329 | 1330 | curKey = { 1331 | 'type': pieces[mainparts.get('type')], 1332 | 'trust_level': pieces[mainparts.get('trust_level')], 1333 | 'trust_level_desc': t, 1334 | 'key_length': pieces[mainparts.get('key_length')], 1335 | 'pubkey_algorithm': z, 1336 | 'keyid': pieces[mainparts.get('keyid')], 1337 | 'creationdate': pieces[mainparts.get('creationdate')], 1338 | 'expirationdate': pieces[mainparts.get('expirationdate')], 1339 | 'key_capabilities': pieces[mainparts.get('key_capabilities')], 1340 | 'hash_algorithm': pieces[mainparts.get('hash_algorithm')], 1341 | 'curve_name': pieces[mainparts.get('curve_name')], 1342 | } 1343 | 1344 | elif curType in follow_lines: 1345 | 1346 | # check follow line type 1347 | if curType == "fpr": 1348 | curKey["fingerprint"] = pieces[followparts.get('userid')] 1349 | fprs.append(curKey["fingerprint"]) 1350 | elif curType == "grp": 1351 | curKey["keygrip"] = pieces[followparts.get('userid')] 1352 | elif curType == "uid": 1353 | curKey["userid"] = pieces[followparts.get('userid')] 1354 | 1355 | # if we make it here we have encountered an unknown linetype 1356 | # we should add the key info we have gathered so far to the keylist 1357 | # and reset the key dict so it won't get added again in case more 1358 | # keys will follow in the next lines 1359 | else: 1360 | if "type" in curKey: 1361 | self._vv("found [{}] key with fingerprint [{}]".format(curKey["type"], curKey["fingerprint"])) 1362 | keys.append(curKey) 1363 | curKey = {} 1364 | 1365 | # after the last line, see if any keys remain which need to be added 1366 | if "type" in curKey: 1367 | self._vv("found [{}] key with fingerprint [{}]".format(curKey["type"], curKey["fingerprint"])) 1368 | keys.append(curKey) 1369 | 1370 | # 1371 | # set and return results 1372 | # 1373 | return { 1374 | 'keys': keys, 1375 | 'fprs': fprs, 1376 | } 1377 | 1378 | 1379 | def compare_trust(self, trust1, trust2): 1380 | """ 1381 | method to compare 2 trust levels 1382 | """ 1383 | # check if we are managing trust 1384 | if not self.module.params["manage_trust"]: 1385 | self._vv("we're not managing trust") 1386 | return True 1387 | 1388 | # 1389 | # trust level returned by GnuPG 1390 | # 'o' : 'Unknown/new', 1391 | # 'i' : 'The key is invalid', 1392 | # 'd' : 'The key has been disabled', 1393 | # 'r' : 'The key has been revoked', 1394 | # 'e' : 'The key has expired', 1395 | # '-' : 'Unknown trust', 1396 | # 'q' : 'Undefined trust', 1397 | # 'n' : 'Dont trust this key at all', 1398 | # 'm' : 'There is marginal trust in this key', 1399 | # 'f' : 'The key is fully trusted', 1400 | # 'u' : 'The key is ultimately trusted', 1401 | # 1402 | trust_map = { 1403 | 'o' : "0", 1404 | 'i' : "0", 1405 | 'd' : "0", 1406 | 'r' : "0", 1407 | 'e' : "0", 1408 | '-' : "1", 1409 | 'q' : "1", 1410 | 'n' : "2", 1411 | 'm' : "3", 1412 | 'f' : "4", 1413 | 'u' : "5", 1414 | } 1415 | 1416 | # convert trust if necessary 1417 | if trust1 in trust_map.keys(): 1418 | trust1 = trust_map[trust1] 1419 | if trust2 in trust_map.keys(): 1420 | trust2 = trust_map[trust2] 1421 | 1422 | self._vv("comparing trust [{}] and [{}]".format(trust1, trust2)) 1423 | 1424 | # compare trust 1425 | return trust1 == trust2 1426 | 1427 | 1428 | def get_trust(self, trust): 1429 | """ 1430 | method to get trust indicator from value 1431 | """ 1432 | gpg_map = { 1433 | '1' : '-', 1434 | '2' : 'n', 1435 | '3' : 'm', 1436 | '4' : 'f', 1437 | '5' : 'u', 1438 | } 1439 | 1440 | trust_map = { 1441 | '-' : 'Unknown trust', 1442 | 'n' : 'Dont trust this key at all', 1443 | 'm' : 'There is marginal trust in this key', 1444 | 'f' : 'The key is fully trusted', 1445 | 'u' : 'The key is ultimately trusted', 1446 | } 1447 | 1448 | # return trust value 1449 | return gpg_map[trust], trust_map[gpg_map[trust]] 1450 | 1451 | 1452 | def set_trust(self, fingerprint, trust): 1453 | """ 1454 | method to set ownertrust 1455 | """ 1456 | # 1457 | # Trust | Description | Value ownertrust | Value with colons 1458 | # 1 | I don't know or won't say | 2 | -|q|o 1459 | # 2 | I do NOT trust | 3 | n 1460 | # 3 | I trust marginally | 4 | m 1461 | # 4 | I trust fully | 5 | f 1462 | # 5 | I trust ultimately | 6 | u 1463 | # 1464 | 1465 | self._vv("update trust level to [{}]".format(trust)) 1466 | 1467 | # IMPORTANT: please keep in mind that with trust levels other than 5 1468 | # the keys you import will need to be signed by a fully trusted key, 1469 | # or be signed using the web of trust; see: 1470 | # Using trust to validate keys: https://www.gnupg.org/gph/en/manual/x334.html 1471 | # signing keys is not handled by this module and should be done by yourself 1472 | # setting the trust. 1473 | 1474 | # trust map 1475 | trust_map = { 1476 | '1' : '2', 1477 | '2' : '3', 1478 | '3' : '4', 1479 | '4' : '5', 1480 | '5' : '6', 1481 | } 1482 | 1483 | # create temporary owner trust file 1484 | # the newline at the end is required to prevent a 'gpg: line too long' error 1485 | content = "{}:{}:\n".format(fingerprint, trust_map[trust]) 1486 | filename = self.prepare_content(content) 1487 | 1488 | # prepare command 1489 | cmd = self.prepare_command("set", "trust") 1490 | cmd += [filename] 1491 | 1492 | # run subprocess 1493 | rc, stdout, stderr = self.module.run_command(args=cmd, check_rc=True) 1494 | 1495 | # delete content 1496 | self.delete_content(filename) 1497 | 1498 | # return 1499 | return True 1500 | 1501 | 1502 | def set_keyinfo(self, keyinfo): 1503 | """ 1504 | sets the keyinfo in an easy to process format 1505 | starting with the fingerprint as the key, 1506 | then the value is a dict with the key details 1507 | """ 1508 | self._vv("setting key info to return to playbook") 1509 | 1510 | # loop through keyinfo and set fprs as dict key 1511 | for key in keyinfo["keys"]: 1512 | if "fingerprint" in key: 1513 | self.result["keys"][key["fingerprint"]] = key 1514 | 1515 | 1516 | def main(): 1517 | 1518 | # define available arguments/parameters a user can pass to the module 1519 | module_args = dict( 1520 | fpr=dict(type='str', required=False), 1521 | keyserver=dict(type='str', default='keyserver.ubuntu.com'), 1522 | file=dict(type='path', required=False), 1523 | content=dict(type='str', required=False), 1524 | trust=dict(type='str', default='1', choices=['1','2','3','4','5']), 1525 | manage_trust=dict(type='bool', default=True), 1526 | state=dict(type='str', default='present', choices=['info', 'present', 'absent', 'latest']), 1527 | gpgbin=dict(type='path', default=None), 1528 | homedir=dict(type='path', default=None), 1529 | keyring=dict(type='path', default=None), 1530 | ) 1531 | 1532 | # set mutually exclusive params 1533 | mutually_exclusive = [ 1534 | ['fpr', 'file', 'content'], 1535 | ] 1536 | 1537 | # set at least one required field 1538 | required_one_of = [ 1539 | ['fpr', 'file', 'content'] 1540 | ] 1541 | 1542 | # the AnsibleModule object will be our abstraction working with Ansible 1543 | # this includes instantiation, a couple of common attr would be the 1544 | # args/params passed to the execution, as well as if the module 1545 | # supports check mode 1546 | module = AnsibleModule( 1547 | argument_spec=module_args, 1548 | mutually_exclusive=mutually_exclusive, 1549 | required_one_of=required_one_of, 1550 | supports_check_mode=True 1551 | ) 1552 | 1553 | # run module 1554 | gpgkey = GpgKey(module) 1555 | result = gpgkey.run() 1556 | 1557 | # in the event of a successful module execution, you will want to 1558 | # simple AnsibleModule.exit_json(), passing the key/value results 1559 | module.exit_json(**result) 1560 | 1561 | 1562 | # if the user is working with this module in only check mode we do not 1563 | # want to make any changes to the environment, just return the current 1564 | # state with no modifications 1565 | if module.check_mode: 1566 | module.exit_json(**result) 1567 | 1568 | # module.get_bin_path / def get_bin_path 1569 | # module.run_command / def run_command 1570 | 1571 | if __name__ == '__main__': 1572 | main() 1573 | --------------------------------------------------------------------------------