Skip to content

Commit da88661

Browse files
authored
Merge branch 'main' into jschladen/fix-tag-check
2 parents b33d94a + 02e1c69 commit da88661

File tree

8 files changed

+260
-23
lines changed

8 files changed

+260
-23
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ jobs:
4747
FORCE_COLOR: 1
4848

4949
steps:
50-
- uses: actions/checkout@v5
50+
- uses: actions/checkout@v6
5151
- name: Set up Python ${{ matrix.python-version }}
5252
uses: actions/setup-python@v6
5353
with:
5454
python-version: ${{ matrix.python-version }}
5555
cache: 'pip'
5656
- name: Set up Node.js 16
57-
uses: actions/setup-node@v5
57+
uses: actions/setup-node@v6
5858
with:
5959
node-version: 16
6060
- name: Install dependencies

.github/workflows/codeql.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ jobs:
4141

4242
steps:
4343
- name: Checkout repository
44-
uses: actions/checkout@v5
44+
uses: actions/checkout@v6
4545

4646
# Install prerequisites for python-ldap. See: https://www.python-ldap.org/en/python-ldap-3.3.0/installing.html#build-prerequisites
4747
- name: Install python-ldap prerequisites
4848
run: sudo apt-get update && sudo apt-get install libldap2-dev libsasl2-dev
4949

5050
# Initializes the CodeQL tools for scanning.
5151
- name: Initialize CodeQL
52-
uses: github/codeql-action/init@v3
52+
uses: github/codeql-action/init@v4
5353
with:
5454
languages: ${{ matrix.language }}
5555
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -63,7 +63,7 @@ jobs:
6363
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
6464
# If this step fails, then you should remove it and run the build manually (see below)
6565
- name: Autobuild
66-
uses: github/codeql-action/autobuild@v3
66+
uses: github/codeql-action/autobuild@v4
6767

6868
# ℹ️ Command-line programs to run using the OS shell.
6969
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -76,6 +76,6 @@ jobs:
7676
# ./location_of_script_within_repo/buildscript.sh
7777

7878
- name: Perform CodeQL Analysis
79-
uses: github/codeql-action/analyze@v3
79+
uses: github/codeql-action/analyze@v4
8080
with:
8181
category: "/language:${{matrix.language}}"

.github/workflows/lemur-publish-release-pypi.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
name: Build distribution
1313
runs-on: ubuntu-latest
1414
steps:
15-
- uses: actions/checkout@v5
15+
- uses: actions/checkout@v6
1616
- name: Set up Python
1717
uses: actions/setup-python@v6
1818
with:
@@ -35,7 +35,7 @@ jobs:
3535
run: |
3636
python setup.py sdist bdist_wheel
3737
- name: Store the distribution packages
38-
uses: actions/upload-artifact@v4
38+
uses: actions/upload-artifact@v5
3939
with:
4040
name: python-package-distributions
4141
path: dist/
@@ -51,7 +51,7 @@ jobs:
5151
id-token: write
5252
steps:
5353
- name: Download all the dists
54-
uses: actions/download-artifact@v5
54+
uses: actions/download-artifact@v6
5555
with:
5656
name: python-package-distributions
5757
path: dist/

docs/administration.rst

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,25 @@ Basic Configuration
276276
DEFAULT_VALIDITY_DAYS = 1095
277277

278278

279+
.. data:: LEMUR_AUTOROTATION_USE_DEFAULT_VALIDITY
280+
:noindex:
281+
282+
When set to True, certificates with autorotation enabled will use the authority's default validity period upon
283+
reissuance, instead of maintaining the original certificate's validity span. This is useful when you want all
284+
autorotated certificates to use a standardized validity period based on the authority's configuration (e.g.,
285+
PUBLIC_CA_DEFAULT_VALIDITY_DAYS for CAB-compliant authorities, or DEFAULT_VALIDITY_DAYS for other authorities).
286+
287+
This feature only affects certificates that have autorotation (rotation flag) enabled. Certificates without
288+
autorotation will continue to maintain their original validity span during reissuance, regardless of this setting.
289+
290+
The default value is False, which maintains backward compatibility by preserving the original certificate's
291+
validity span during reissuance.
292+
293+
::
294+
295+
LEMUR_AUTOROTATION_USE_DEFAULT_VALIDITY = False
296+
297+
279298
.. data:: DEBUG_DUMP
280299
:noindex:
281300

@@ -546,10 +565,44 @@ to enable it, you must set the option ``--notify`` (when using cron) or the conf
546565
Use this config (optional) to migrate from one authority id to another on reissuance (useful for expiring authorities,
547566
key migrations, etc).
548567

549-
::
568+
This configuration supports two types of values:
569+
570+
1. **Static mapping** (integer): Maps an old authority ID directly to a new authority ID.
571+
2. **Dynamic callback** (callable): A function that receives a certificate object and returns the new authority ID based on certificate properties.
572+
573+
Static mapping example::
550574

551575
ROTATE_AUTHORITY_TRANSLATION = {1: 2}
552576

577+
Callback function example::
578+
579+
def select_authority_for_renewal(certificate):
580+
"""
581+
Dynamically determine the authority for certificate renewal.
582+
583+
Args:
584+
certificate: Certificate object with properties like:
585+
- owner: str
586+
- destinations: list of Destination objects
587+
- key_type: str
588+
- cn: str
589+
- san: str
590+
- authority: Authority object
591+
- authority_id: int
592+
593+
Returns:
594+
int: The new authority ID to use
595+
"""
596+
# Example: Select authority based on destinations
597+
destination_labels = [dest.label for dest in certificate.destinations]
598+
if 'production-aws' in destination_labels:
599+
return 3 # Use cross-signed chain for production AWS
600+
elif 'legacy-systems' in destination_labels:
601+
return 4 # Use older authority for legacy compatibility
602+
return certificate.authority_id # Keep original authority
603+
604+
ROTATE_AUTHORITY_TRANSLATION = {1: select_authority_for_renewal}
605+
553606

554607
**Certificate rotation**
555608

@@ -1723,16 +1776,33 @@ The following configuration properties are optional when using the Digicert CIS
17231776
Defines the default signing algorithm for a given issuer name e.g. {"Digicert": "sha1"} will result in sha1 certs issued with the Digicert issuer (default = {}).
17241777

17251778

1779+
1780+
1781+
.. data:: DIGICERT_CIS_USE_CSR_FIELDS
1782+
:noindex:
1783+
1784+
Controls the setting of the `use_csr_fields` parameter of the create certificate endpoint. When set, certificates will be issued with values from the csr instead of via API fields (default = False).
1785+
1786+
17261787
.. data:: DIGICERT_CIS_ROOTS
17271788
:noindex:
17281789

1729-
A string->string mapping from issuer name to root PEM. These will be optionally be appended to / stripped from response chains as requested by users.
1790+
A string->string mapping from authority name to root certificate PEM. This is used during authority creation to store the root certificate in Lemur's database.
17301791

17311792

1732-
.. data:: DIGICERT_CIS_USE_CSR_FIELDS
1793+
.. data:: DIGICERT_CIS_ALTERNATE_CHAINS
17331794
:noindex:
17341795

1735-
Controls the setting of the `use_csr_fields` parameter of the create certificate endpoint. When set, certificates will be issued with values from the csr instead of via API fields (default = False).
1796+
A string->string mapping from authority name to alternate/cross-signed chain PEM. When configured, the specified chain will be appended to certificates issued by that authority. This is useful for providing cross-signed roots for compatibility with older systems.
1797+
1798+
Example::
1799+
1800+
DIGICERT_CIS_ALTERNATE_CHAINS = {
1801+
"DigiCert-G2-RSA-Cross-Global": """-----BEGIN CERTIFICATE-----
1802+
MIIEgjCCA2qgAwIBAgIQBEbB7LuEYrWpF3L5qhjmezANBgkqhkiG9w0BAQsFADBh
1803+
...
1804+
-----END CERTIFICATE-----"""
1805+
}
17361806

17371807

17381808
CFSSL Issuer Plugin

lemur/auth/service.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
.. moduleauthor:: Kevin Glisson <[email protected]>
99
1010
"""
11+
import binascii
1112
import json
1213
from datetime import datetime, timedelta
1314
from functools import wraps
1415

15-
import binascii
1616
import jwt
1717
from cryptography.hazmat.backends import default_backend
1818
from cryptography.hazmat.primitives import serialization, hashes
@@ -167,6 +167,12 @@ def decorated_function(*args, **kwargs):
167167
if not g.current_user:
168168
return dict(message="You are not logged in"), 403
169169

170+
metrics.send("user_authentication", "counter", 1,
171+
metric_tags={"application_name": getattr(g, "caller_application", "none"),
172+
"user_id": g.current_user.id,
173+
"endpoint": request.endpoint,
174+
"aid": payload.get("aid", "none")})
175+
170176
# Tell Flask-Principal the identity changed
171177
identity_changed.send(
172178
current_app._get_current_object(), identity=Identity(g.current_user.id)

lemur/certificates/service.py

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -933,17 +933,32 @@ def get_name_from_arn(arn):
933933
return arn.split("/", 1)[1]
934934

935935

936-
def calculate_reissue_range(start, end):
936+
def calculate_reissue_range(start, end, authority=None, rotation=False):
937937
"""
938938
Determine what the new validity_start and validity_end dates should be.
939939
:param start:
940940
:param end:
941+
:param authority: Optional authority to use for default validity calculation
942+
:param rotation: Whether this certificate has autorotation enabled
941943
:return:
942944
"""
943-
span = end - start
944-
945945
new_start = arrow.utcnow()
946-
new_end = new_start + span
946+
947+
# Check if we should use default validity for autorotation reissues
948+
use_default_validity = (
949+
rotation
950+
and authority
951+
and current_app.config.get("LEMUR_AUTOROTATION_USE_DEFAULT_VALIDITY")
952+
)
953+
954+
if use_default_validity:
955+
# Use authority's default validity days
956+
default_days = authority.default_validity_days
957+
new_end = new_start.shift(days=default_days)
958+
else:
959+
# Use original certificate's validity span
960+
span = end - start
961+
new_end = new_start + span
947962

948963
return new_start, arrow.get(new_end)
949964

@@ -956,7 +971,12 @@ def get_certificate_primitives(certificate):
956971
:return: dict of certificate primitives, should be enough to effectively re-issue
957972
certificate via `create`.
958973
"""
959-
start, end = calculate_reissue_range(certificate.not_before, certificate.not_after)
974+
start, end = calculate_reissue_range(
975+
certificate.not_before,
976+
certificate.not_after,
977+
authority=certificate.authority,
978+
rotation=certificate.rotation
979+
)
960980
ser = CertificateInputSchema().load(
961981
CertificateOutputSchema().dump(certificate).data
962982
)
@@ -1001,10 +1021,40 @@ def reissue_certificate(certificate, notify=None, replace=None, user=None):
10011021
if replace:
10021022
primitives["replaces"] = [certificate]
10031023

1004-
if primitives["authority"].id in current_app.config.get("ROTATE_AUTHORITY_TRANSLATION", {}):
1005-
primitives["authority"] = database.get(Authority,
1006-
current_app.config.get("ROTATE_AUTHORITY_TRANSLATION", {})[primitives["authority"].id]
1007-
)
1024+
# Support both static authority ID mapping and dynamic callback functions
1025+
authority_translation = current_app.config.get("ROTATE_AUTHORITY_TRANSLATION", {})
1026+
if primitives["authority"].id in authority_translation:
1027+
original_authority_id = primitives["authority"].id
1028+
original_authority_name = primitives["authority"].name
1029+
translation_value = authority_translation[primitives["authority"].id]
1030+
1031+
# Check if the value is a callable (function)
1032+
if callable(translation_value):
1033+
# Call the function with the certificate to determine the new authority ID
1034+
new_authority_id = translation_value(certificate)
1035+
if new_authority_id is not None:
1036+
primitives["authority"] = database.get(Authority, new_authority_id)
1037+
else:
1038+
# Static integer mapping (original behavior)
1039+
new_authority_id = translation_value
1040+
primitives["authority"] = database.get(Authority, translation_value)
1041+
1042+
# Log and metric the translation
1043+
if new_authority_id is not None:
1044+
current_app.logger.info(
1045+
f"Authority translated for certificate {certificate.name}: "
1046+
f"{original_authority_name} (ID: {original_authority_id}) -> "
1047+
f"{primitives['authority'].name} (ID: {new_authority_id})"
1048+
)
1049+
metrics.send(
1050+
"certificate_authority_translation",
1051+
"counter",
1052+
1,
1053+
metric_tags={
1054+
"original_authority": original_authority_name,
1055+
"new_authority": primitives["authority"].name,
1056+
}
1057+
)
10081058

10091059
# Modify description to include the certificate ID being reissued and mention that this is created by Lemur
10101060
# as part of reissue

lemur/plugins/lemur_digicert/plugin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,14 @@ def create_certificate(self, csr, issuer_options):
685685
end_entity = certificate_chain_pem[0]
686686
intermediate = certificate_chain_pem[1]
687687

688+
# Append cross-signed root if configured for this authority
689+
authority_name = issuer_options.get('authority').name if issuer_options.get('authority') else None
690+
if authority_name:
691+
cross_signed_root = current_app.config.get("DIGICERT_CIS_ALTERNATE_CHAINS", {}).get(authority_name)
692+
if cross_signed_root:
693+
# Append the cross-signed root to the intermediate chain
694+
intermediate = str(intermediate) + "\n" + cross_signed_root
695+
688696
return (
689697
"\n".join(str(end_entity).splitlines()),
690698
"\n".join(str(intermediate).splitlines()),

0 commit comments

Comments
 (0)