1"""
2Install certificates into the keychain on Mac OS
3
4.. versionadded:: 2016.3.0
5
6"""
7
8import logging
9import re
10import shlex
11
12import salt.utils.platform
13
14try:
15    import pipes
16
17    HAS_DEPS = True
18except ImportError:
19    HAS_DEPS = False
20
21if hasattr(shlex, "quote"):
22    _quote = shlex.quote
23elif HAS_DEPS and hasattr(pipes, "quote"):
24    _quote = pipes.quote
25else:
26    _quote = None
27
28log = logging.getLogger(__name__)
29
30__virtualname__ = "keychain"
31
32
33def __virtual__():
34    """
35    Only work on Mac OS
36    """
37    if salt.utils.platform.is_darwin() and _quote is not None:
38        return __virtualname__
39    return (False, "Only available on Mac OS systems with pipes")
40
41
42def install(
43    cert,
44    password,
45    keychain="/Library/Keychains/System.keychain",
46    allow_any=False,
47    keychain_password=None,
48):
49    """
50    Install a certificate
51
52    cert
53        The certificate to install
54
55    password
56        The password for the certificate being installed formatted in the way
57        described for openssl command in the PASS PHRASE ARGUMENTS section.
58
59        Note: The password given here will show up as plaintext in the job returned
60        info.
61
62    keychain
63        The keychain to install the certificate to, this defaults to
64        /Library/Keychains/System.keychain
65
66    allow_any
67        Allow any application to access the imported certificate without warning
68
69    keychain_password
70        If your keychain is likely to be locked pass the password and it will be unlocked
71        before running the import
72
73        Note: The password given here will show up as plaintext in the returned job
74        info.
75
76    CLI Example:
77
78    .. code-block:: bash
79
80        salt '*' keychain.install test.p12 test123
81    """
82    if keychain_password is not None:
83        unlock_keychain(keychain, keychain_password)
84
85    cmd = "security import {} -P {} -k {}".format(cert, password, keychain)
86    if allow_any:
87        cmd += " -A"
88    return __salt__["cmd.run"](cmd)
89
90
91def uninstall(
92    cert_name, keychain="/Library/Keychains/System.keychain", keychain_password=None
93):
94    """
95    Uninstall a certificate from a keychain
96
97    cert_name
98        The name of the certificate to remove
99
100    keychain
101        The keychain to install the certificate to, this defaults to
102        /Library/Keychains/System.keychain
103
104    keychain_password
105        If your keychain is likely to be locked pass the password and it will be unlocked
106        before running the import
107
108        Note: The password given here will show up as plaintext in the returned job
109        info.
110
111    CLI Example:
112
113    .. code-block:: bash
114
115        salt '*' keychain.install test.p12 test123
116    """
117    if keychain_password is not None:
118        unlock_keychain(keychain, keychain_password)
119
120    cmd = 'security delete-certificate -c "{}" {}'.format(cert_name, keychain)
121    return __salt__["cmd.run"](cmd)
122
123
124def list_certs(keychain="/Library/Keychains/System.keychain"):
125    """
126    List all of the installed certificates
127
128    keychain
129        The keychain to install the certificate to, this defaults to
130        /Library/Keychains/System.keychain
131
132    CLI Example:
133
134    .. code-block:: bash
135
136        salt '*' keychain.list_certs
137    """
138    cmd = (
139        'security find-certificate -a {} | grep -o "alis".*\\" | '
140        "grep -o '\\\"[-A-Za-z0-9.:() ]*\\\"'".format(_quote(keychain))
141    )
142    out = __salt__["cmd.run"](cmd, python_shell=True)
143    return out.replace('"', "").split("\n")
144
145
146def get_friendly_name(cert, password):
147    """
148    Get the friendly name of the given certificate
149
150    cert
151        The certificate to install
152
153    password
154        The password for the certificate being installed formatted in the way
155        described for openssl command in the PASS PHRASE ARGUMENTS section
156
157        Note: The password given here will show up as plaintext in the returned job
158        info.
159
160    CLI Example:
161
162    .. code-block:: bash
163
164        salt '*' keychain.get_friendly_name /tmp/test.p12 test123
165    """
166    cmd = (
167        "openssl pkcs12 -in {} -passin pass:{} -info -nodes -nokeys 2> /dev/null | "
168        "grep friendlyName:".format(_quote(cert), _quote(password))
169    )
170    out = __salt__["cmd.run"](cmd, python_shell=True)
171    return out.replace("friendlyName: ", "").strip()
172
173
174def get_default_keychain(user=None, domain="user"):
175    """
176    Get the default keychain
177
178    user
179        The user to check the default keychain of
180
181    domain
182        The domain to use valid values are user|system|common|dynamic, the default is user
183
184    CLI Example:
185
186    .. code-block:: bash
187
188        salt '*' keychain.get_default_keychain
189    """
190    cmd = "security default-keychain -d {}".format(domain)
191    return __salt__["cmd.run"](cmd, runas=user)
192
193
194def set_default_keychain(keychain, domain="user", user=None):
195    """
196    Set the default keychain
197
198    keychain
199        The location of the keychain to set as default
200
201    domain
202        The domain to use valid values are user|system|common|dynamic, the default is user
203
204    user
205        The user to set the default keychain as
206
207    CLI Example:
208
209    .. code-block:: bash
210
211        salt '*' keychain.set_keychain /Users/fred/Library/Keychains/login.keychain
212    """
213    cmd = "security default-keychain -d {} -s {}".format(domain, keychain)
214    return __salt__["cmd.run"](cmd, runas=user)
215
216
217def unlock_keychain(keychain, password):
218    """
219    Unlock the given keychain with the password
220
221    keychain
222        The keychain to unlock
223
224    password
225        The password to use to unlock the keychain.
226
227        Note: The password given here will show up as plaintext in the returned job
228        info.
229
230    CLI Example:
231
232    .. code-block:: bash
233
234        salt '*' keychain.unlock_keychain /tmp/test.p12 test123
235    """
236    cmd = "security unlock-keychain -p {} {}".format(password, keychain)
237    __salt__["cmd.run"](cmd)
238
239
240def get_hash(name, password=None):
241    """
242    Returns the hash of a certificate in the keychain.
243
244    name
245        The name of the certificate (which you can get from keychain.get_friendly_name) or the
246        location of a p12 file.
247
248    password
249        The password that is used in the certificate. Only required if your passing a p12 file.
250        Note: This will be outputted to logs
251
252    CLI Example:
253
254    .. code-block:: bash
255
256        salt '*' keychain.get_hash /tmp/test.p12 test123
257    """
258
259    if ".p12" in name[-4:]:
260        cmd = "openssl pkcs12 -in {0} -passin pass:{1} -passout pass:{1}".format(
261            name, password
262        )
263    else:
264        cmd = 'security find-certificate -c "{}" -m -p'.format(name)
265
266    out = __salt__["cmd.run"](cmd)
267    matches = re.search(
268        "-----BEGIN CERTIFICATE-----(.*)-----END CERTIFICATE-----",
269        out,
270        re.DOTALL | re.MULTILINE,
271    )
272    if matches:
273        return matches.group(1)
274    else:
275        return False
276