• 
      

    Add a sense of purpose to SSL/TLS certificates.

    Review Request #14896 — Created March 11, 2026 and updated

    Information

    Review Board
    release-7.1.x

    Reviewers

    My original design for SSL/TLS certificate management combined two
    important purposes into one concept of a certificate. Depending on the
    call site and the presence of a private key, a Certificate could be
    used for verifying a server or authenticating the client with a server.
    These are very different purposes, and needed to be managed separately.

    The original design led to incorrect calls in build_ssl_context() and
    therefore build_urlopen_kwargs(), meaning we didn't always have the
    right context in place to handle the operation.

    This change addresses this by:

    1. Introducing a CertPurpose enum containing TRUST (for
      cert trust/verification needs) and CLIENT (for client
      authentication).

    2. A purpose field in Certificate and all cert-related operations in
      CertificateManager, and storage equivalents in the backends.

    A "trust" cert may not have a private key, and a "client" cert must
    (although technically that cert could embed the key, we're requiring two
    separate files in our implementation for now).

    This does change the signatures and expectations in the storage
    backends, and we're not going through a deprecation cycle for that. The
    reason being that, while we introduced all this in Review Board 6, we
    never made actual use of it. Going through a deprecation cycle would
    introduce a fair amount of complexity, and isn't worth it for something
    that users haven't touched or would have had a need to build upon.

    One of the backwards-incompatible changes is the location of files in
    the file storage backend. Both kinds of certs used to live in certs/,
    but now they live in certs/client/ and certs/trust/ instead.

    The change is pretty straight-forward. The biggest change in behavior is
    build_ssl_context(), which now passes the right kinds of certs to the
    right functions.

    The bulk of the change is actually unit tests and testdata file
    movement. We now have trust and client purpose variations for each
    relevant test.

    As a note, there's some initial attempts at modernizing some of the
    typing in this change, but it's purposefully incomplete. The rest will
    be tackled separately.

    All unit tests pass.

    Summary ID
    Add a sense of purpose to SSL/TLS certificates.
    My original design for SSL/TLS certificate management combined two important purposes into one concept of a certificate. Depending on the call site and the presence of a private key, a `Certificate` could be used for verifying a server or authenticating the client with a server. These are very different purposes, and needed to be managed separately. The original design led to incorrect calls in `build_ssl_context()` and therefore `build_urlopen_kwargs()`, meaning we didn't always have the right context in place to handle the operation. This change addresses this by: 1. Introducing a `CertPurpose` enum containing `TRUST` (for cert trust/verification needs) and `CLIENT` (for client authentication). 2. A `purpose` field in `Certificate` and all cert-related operations in `CertificateManager`, and storage equivalents in the backends. A "trust" cert may not have a private key, and a "client" cert must (although technically that cert could embed the key, we're requiring two separate files in our implementation for now). This does change the signatures and expectations in the storage backends, and we're not going through a deprecation cycle for that. The reason being that, while we introduced all this in Review Board 6, we never made actual use of it. Going through a deprecation cycle would introduce a fair amount of complexity, and isn't worth it for something that users haven't touched or would have had a need to build upon. One of the backwards-incompatible changes is the location of files in the file storage backend. Both kinds of certs used to live in `certs/`, but now they live in `certs/client/` and `certs/purpose/` instead. The change is pretty straight-forward. The biggest change in behavior is `build_ssl_context()`, which now passes the right kinds of certs to the right functions. The bulk of the change is actually unit tests and testdata file movement. We now have trust and client purpose variations for each relevant test.
    7853c5eed80c779e4d65c33a3dce5065ee43da0c
    Description From Last Updated

    Your description says certs/purpose/ where I think it should say certs/trust/

    daviddavid

    'typing.cast' imported but unused Column: 1 Error code: F401

    reviewbotreviewbot

    too many blank lines (2) Column: 5 Error code: E303

    reviewbotreviewbot

    local variable 'local_site1' is assigned to but never used Column: 9 Error code: F841

    reviewbotreviewbot

    This should use typelets.runtime.raise_invalid_type() so type checkers don't flag this branch as unreachable.

    daviddavid

    It seems like this should happen before we validate key_path

    daviddavid

    This should use typelets.runtime.raise_invalid_type() so type checkers don't flag this branch as unreachable.

    daviddavid

    Since this class is already an ABC subclass, perhaps we should wrap this method in @abstractmethod, so that it's not …

    daviddavid

    This doesn't seem right. _build_fingerprints_file_path doesn't take a purpose argument, and should still return <hostname>__<port>.json .

    daviddavid

    This isn't a new issue with the change, but a bunch of the docstrings here say create_form_files instead of create_from_files

    daviddavid

    Mind fixing this while you're here? founD -> found

    daviddavid

    Can we log something if there's a client cert path but no key file?

    daviddavid

    Purpose is required, so we can get rid of "if specified" in here.

    daviddavid
    Checks run (1 failed, 1 succeeded)
    flake8 failed.
    JSHint passed.

    flake8

    chipx86
    david
    1. 
        
    2. Show all issues

      Your description says certs/purpose/ where I think it should say certs/trust/

    3. reviewboard/certs/cert.py (Diff revision 2)
       
       
       
       
       
      Show all issues

      This should use typelets.runtime.raise_invalid_type() so type checkers don't flag this branch as unreachable.

    4. reviewboard/certs/cert.py (Diff revision 2)
       
       
       
       
       
       
      Show all issues

      It seems like this should happen before we validate key_path

      1. I have this organized as:

        1. Argument combinations (it's not key_path, it's the combination of purpose + key_path, and if we ever support passwords for keys then we'd include that here too).
        2. Cert and key path values (including existence).
        3. Contents of the files we loaded.
    5. reviewboard/certs/cert.py (Diff revision 2)
       
       
       
       
       
      Show all issues

      This should use typelets.runtime.raise_invalid_type() so type checkers don't flag this branch as unreachable.

    6. reviewboard/certs/storage/base.py (Diff revision 2)
       
       
      Show all issues

      Since this class is already an ABC subclass, perhaps we should wrap this method in @abstractmethod, so that it's not even possible to define a subclass that doesn't implement this?

      1. The challenge with this (and something I want to address in housekeeping) is that we then break interfaces at import time rather than runtime, which completely alters the entire backwards-compatibility story. If this were, say, an SCMTool, then doing this would break any legacy tools right out of the box. I'd rather avoid this until we have a better solution we can use that preserves this.

    7. Show all issues

      This doesn't seem right. _build_fingerprints_file_path doesn't take a purpose argument, and should still return <hostname>__<port>.json .

      1. Holdover from an earlier attempt at this work. All these are wrong. Fixing.

    8. Show all issues

      This isn't a new issue with the change, but a bunch of the docstrings here say create_form_files instead of create_from_files

    9. 
        
    chipx86
    david
    1. 
        
    2. reviewboard/certs/cert.py (Diff revision 3)
       
       
      Show all issues

      Mind fixing this while you're here? founD -> found

    3. reviewboard/certs/manager.py (Diff revision 3)
       
       
       
       
       
       
       
      Show all issues

      Can we log something if there's a client cert path but no key file?

    4. reviewboard/certs/storage/base.py (Diff revision 3)
       
       
       
       
      Show all issues

      Purpose is required, so we can get rid of "if specified" in here.

    5. 
        
    chipx86
    Review request changed
    Change Summary:
    • Updated versions to RB 8.0.
    • Fixed some issues in docs.
    • Added logging if a client cert was found but a key was not.
    Commits:
    Summary ID
    Add a sense of purpose to SSL/TLS certificates.
    My original design for SSL/TLS certificate management combined two important purposes into one concept of a certificate. Depending on the call site and the presence of a private key, a `Certificate` could be used for verifying a server or authenticating the client with a server. These are very different purposes, and needed to be managed separately. The original design led to incorrect calls in `build_ssl_context()` and therefore `build_urlopen_kwargs()`, meaning we didn't always have the right context in place to handle the operation. This change addresses this by: 1. Introducing a `CertPurpose` enum containing `TRUST` (for cert trust/verification needs) and `CLIENT` (for client authentication). 2. A `purpose` field in `Certificate` and all cert-related operations in `CertificateManager`, and storage equivalents in the backends. A "trust" cert may not have a private key, and a "client" cert must (although technically that cert could embed the key, we're requiring two separate files in our implementation for now). This does change the signatures and expectations in the storage backends, and we're not going through a deprecation cycle for that. The reason being that, while we introduced all this in Review Board 6, we never made actual use of it. Going through a deprecation cycle would introduce a fair amount of complexity, and isn't worth it for something that users haven't touched or would have had a need to build upon. One of the backwards-incompatible changes is the location of files in the file storage backend. Both kinds of certs used to live in `certs/`, but now they live in `certs/client/` and `certs/purpose/` instead. The change is pretty straight-forward. The biggest change in behavior is `build_ssl_context()`, which now passes the right kinds of certs to the right functions. The bulk of the change is actually unit tests and testdata file movement. We now have trust and client purpose variations for each relevant test.
    e03270282a569a1a8251bd2d57dc1f718cd1e828
    Add a sense of purpose to SSL/TLS certificates.
    My original design for SSL/TLS certificate management combined two important purposes into one concept of a certificate. Depending on the call site and the presence of a private key, a `Certificate` could be used for verifying a server or authenticating the client with a server. These are very different purposes, and needed to be managed separately. The original design led to incorrect calls in `build_ssl_context()` and therefore `build_urlopen_kwargs()`, meaning we didn't always have the right context in place to handle the operation. This change addresses this by: 1. Introducing a `CertPurpose` enum containing `TRUST` (for cert trust/verification needs) and `CLIENT` (for client authentication). 2. A `purpose` field in `Certificate` and all cert-related operations in `CertificateManager`, and storage equivalents in the backends. A "trust" cert may not have a private key, and a "client" cert must (although technically that cert could embed the key, we're requiring two separate files in our implementation for now). This does change the signatures and expectations in the storage backends, and we're not going through a deprecation cycle for that. The reason being that, while we introduced all this in Review Board 6, we never made actual use of it. Going through a deprecation cycle would introduce a fair amount of complexity, and isn't worth it for something that users haven't touched or would have had a need to build upon. One of the backwards-incompatible changes is the location of files in the file storage backend. Both kinds of certs used to live in `certs/`, but now they live in `certs/client/` and `certs/purpose/` instead. The change is pretty straight-forward. The biggest change in behavior is `build_ssl_context()`, which now passes the right kinds of certs to the right functions. The bulk of the change is actually unit tests and testdata file movement. We now have trust and client purpose variations for each relevant test.
    7853c5eed80c779e4d65c33a3dce5065ee43da0c

    Checks run (2 succeeded)

    flake8 passed.
    JSHint passed.
    david
    1. Ship It!
    2.