Repository: certbot/certbot

Issue:
--cert-path prepends a numeric prefix to the filename
When using `--csr` and `--cert-path`, the certificate is placed in a different location than what was specified by the user.

```
root@connor:~# /usr/local/bin/openssl req -new -newkey rsa:2048 -keyout dank.ninja.key -nodes -subj "/CN=dank.ninja" -reqexts SAN -config <( cat /usr/local/etc/ssl/openssl.cnf; echo "[SAN]"; echo subjectAltName=DNS:dank.ninja,DNS:www.dank.ninja ) -outform der -out dank.ninja.csr
Generating a 2048 bit RSA private key
.......................+++
....................................................+++
writing new private key to 'dank.ninja'
-----
```

```
root@connor:~# letsencrypt certonly --csr dank.ninja.csr --cert-path dank.ninja.crt

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /root/0001_chain.pem. Your cert will expire on 2016-02-12. To
   obtain a new version of the certificate in the future, simply run
   Let's Encrypt again.
```

```
root@connor:~# ls dank.ninja.crt
ls: dank.ninja.crt: No such file or directory
```

```
root@connor:~# ls 0000_dank.ninja.crt
0000_dank.ninja.crt
```



Discussion:
@sagi is going to take a look at this.

Same with --chain-path and --fullchain-path, both get prefixed too. Either stop prefixing or set symlinks like in /etc/letsencrypt/live/example.com where current certs are symlinks to /etc/letsencrypt/archive/example.com/

Are there any news when this bug will be fixed? Because we want to use --fullchain-path and --cert-path parameter (we want to use the same private key, unless it becomes compromised, so that we can use hpkp with longer time periods). But right now, after every single renewal I have to "fix" the httpd ssl config or have to remove the numeric prefix and reload httpd (okay reloading httpd is inevitable either way).

Thanks for bumping it up.
@bmw: the `save_certificate(...)` function is triggered and saves the chain and crt files with unique file names that were generated by the  `le_util.unique_file(...)` function.

I'm currently thinking on creating a default arg `(..., unique_files=false)` that will be responsible for that. 

Have you got any thoughts on this?

@sagi, something like that would work. If `cert_path` is set on the command line, you can set `unique_files` to `False`. You would probably want the default of `unique_files` to be `True` to not break behavior elsewhere though.

With that said, it might be easier to just call [safe_open](https://github.com/letsencrypt/letsencrypt/blob/master/certbot/le_util.py#L154) directly with the arguments you want if `cert_path` is set.


Resolving patch:
diff --git a/certbot/client.py b/certbot/client.py
--- a/certbot/client.py
+++ b/certbot/client.py
@@ -24,6 +24,7 @@
 from certbot import le_util
 from certbot import reverter
 from certbot import storage
+from certbot import cli
 
 from certbot.display import ops as display_ops
 from certbot.display import enhancements
@@ -317,23 +318,30 @@ def save_certificate(self, certr, chain_cert,
 
         cert_pem = OpenSSL.crypto.dump_certificate(
             OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped)
-        cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644)
+
+        cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path)
+
         try:
             cert_file.write(cert_pem)
         finally:
             cert_file.close()
         logger.info("Server issued certificate; certificate written to %s",
-                    act_cert_path)
+                    abs_cert_path)
 
-        cert_chain_abspath = None
-        fullchain_abspath = None
-        if chain_cert:
+        if not chain_cert:
+            return abs_cert_path, None, None
+        else:
             chain_pem = crypto_util.dump_pyopenssl_chain(chain_cert)
-            cert_chain_abspath = _save_chain(chain_pem, chain_path)
-            fullchain_abspath = _save_chain(cert_pem + chain_pem,
-                                            fullchain_path)
 
-        return os.path.abspath(act_cert_path), cert_chain_abspath, fullchain_abspath
+            chain_file, abs_chain_path =\
+                    _open_pem_file('chain_path', chain_path)
+            fullchain_file, abs_fullchain_path =\
+                    _open_pem_file('fullchain_path', fullchain_path)
+
+            _save_chain(chain_pem, chain_file)
+            _save_chain(cert_pem + chain_pem, fullchain_file)
+
+            return abs_cert_path, abs_chain_path, abs_fullchain_path
 
     def deploy_certificate(self, domains, privkey_path,
                            cert_path, chain_path, fullchain_path):
@@ -565,24 +573,35 @@ def view_config_changes(config, num=None):
     rev.recovery_routine()
     rev.view_config_changes(num)
 
+def _open_pem_file(cli_arg_path, pem_path):
+    """Open a pem file.
+
+    If cli_arg_path was set by the client, open that.
+    Otherwise, uniquify the file path.
+
+    :param str cli_arg_path: the cli arg name, e.g. cert_path
+    :param str pem_path: the pem file path to open
 
-def _save_chain(chain_pem, chain_path):
+    :returns: a tuple of file object and its absolute file path
+
+    """
+    if cli.set_by_cli(cli_arg_path):
+        return le_util.safe_open(pem_path, chmod=0o644),\
+            os.path.abspath(pem_path)
+    else:
+        uniq = le_util.unique_file(pem_path, 0o644)
+        return uniq[0], os.path.abspath(uniq[1])
+
+def _save_chain(chain_pem, chain_file):
     """Saves chain_pem at a unique path based on chain_path.
 
     :param str chain_pem: certificate chain in PEM format
-    :param str chain_path: candidate path for the cert chain
-
-    :returns: absolute path to saved cert chain
-    :rtype: str
+    :param str chain_file: chain file object
 
     """
-    chain_file, act_chain_path = le_util.unique_file(chain_path, 0o644)
     try:
         chain_file.write(chain_pem)
     finally:
         chain_file.close()
 
-    logger.info("Cert chain written to %s", act_chain_path)
-
-    # This expects a valid chain file
-    return os.path.abspath(act_chain_path)
+    logger.info("Cert chain written to %s", chain_file.name)
diff --git a/certbot/le_util.py b/certbot/le_util.py
--- a/certbot/le_util.py
+++ b/certbot/le_util.py
@@ -151,7 +151,8 @@ def _unique_file(path, filename_pat, count, mode):
     while True:
         current_path = os.path.join(path, filename_pat(count))
         try:
-            return safe_open(current_path, chmod=mode), current_path
+            return safe_open(current_path, chmod=mode),\
+                os.path.abspath(current_path)
         except OSError as err:
             # "File exists," is okay, try a different name.
             if err.errno != errno.EEXIST:


Test patch:
diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py
--- a/certbot/tests/client_test.py
+++ b/certbot/tests/client_test.py
@@ -221,7 +221,9 @@ def test_obtain_certificate(self, mock_crypto_util):
             mock.sentinel.key, domains, self.config.csr_dir)
         self._check_obtain_certificate()
 
-    def test_save_certificate(self):
+    @mock.patch("certbot.cli.helpful_parser")
+    def test_save_certificate(self, mock_parser):
+        # pylint: disable=too-many-locals
         certs = ["matching_cert.pem", "cert.pem", "cert-san.pem"]
         tmp_path = tempfile.mkdtemp()
         os.chmod(tmp_path, 0o755)  # TODO: really??
@@ -232,6 +234,10 @@ def test_save_certificate(self):
         candidate_cert_path = os.path.join(tmp_path, "certs", "cert.pem")
         candidate_chain_path = os.path.join(tmp_path, "chains", "chain.pem")
         candidate_fullchain_path = os.path.join(tmp_path, "chains", "fullchain.pem")
+        mock_parser.verb = "certonly"
+        mock_parser.args = ["--cert-path", candidate_cert_path,
+                "--chain-path", candidate_chain_path,
+                "--fullchain-path", candidate_fullchain_path]
 
         cert_path, chain_path, fullchain_path = self.client.save_certificate(
             certr, chain_cert, candidate_cert_path, candidate_chain_path,
diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh
--- a/tests/boulder-integration.sh
+++ b/tests/boulder-integration.sh
@@ -43,8 +43,8 @@ export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
 common auth --csr "$CSR_PATH" \
        --cert-path "${root}/csr/cert.pem" \
        --chain-path "${root}/csr/chain.pem"
-openssl x509 -in "${root}/csr/0000_cert.pem" -text
-openssl x509 -in "${root}/csr/0000_chain.pem" -text
+openssl x509 -in "${root}/csr/cert.pem" -text
+openssl x509 -in "${root}/csr/chain.pem" -text
 
 common --domains le3.wtf install \
        --cert-path "${root}/csr/cert.pem" \