Skip to content

Commit 1d6eacb

Browse files
committed
#KDB-850 add certificate validity period metrics
1 parent 5d84698 commit 1d6eacb

File tree

2 files changed

+242
-0
lines changed

2 files changed

+242
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (c) Kurrent, Inc and/or licensed to Kurrent, Inc under one or more agreements.
2+
// Kurrent, Inc licenses this file to you under the Kurrent License v1 (see LICENSE.md).
3+
4+
using System;
5+
using System.Diagnostics.Metrics;
6+
using System.Linq;
7+
using System.Security.Cryptography;
8+
using System.Security.Cryptography.X509Certificates;
9+
using KurrentDB.Core.Certificates;
10+
using KurrentDB.Core.Metrics;
11+
using KurrentDB.Core.Time;
12+
using Xunit;
13+
14+
15+
namespace KurrentDB.Core.XUnit.Tests.Metrics;
16+
17+
public class CertificatesMetricTests : IDisposable {
18+
private readonly TestMeterListener<double> _doubleListener;
19+
20+
private readonly Meter _meter;
21+
private readonly DateTimeOffset _now;
22+
23+
public CertificatesMetricTests() {
24+
_meter = new Meter($"{typeof(CertificatesMetricTests)}");
25+
_doubleListener = new TestMeterListener<double>(_meter);
26+
_doubleListener.Observe();
27+
_now = DateTimeOffset.FromUnixTimeSeconds(Clock.Instance.SecondsSinceEpoch);
28+
}
29+
30+
public void Dispose() => _doubleListener.Dispose();
31+
32+
[Fact]
33+
public void When_Expired() {
34+
35+
36+
var b = new CertificateProviderTest(_now.AddDays(-10), _now.AddDays(-9));
37+
var sut = new CertificatesMetric(_meter, "cert_apocalypse", b);
38+
39+
40+
sut.Measure();
41+
var measurements = _doubleListener.RetrieveMeasurements("cert_apocalypse-day");
42+
43+
Assert.Contains(
44+
measurements,
45+
m => {
46+
47+
Assert.True(Math.Abs(m.Value + 9) < 0.01);
48+
49+
50+
Assert.Equal(CertificatesMetric.Tags.All, m.Tags.Select(t => t.Key));
51+
Assert.Contains(
52+
m.Tags.Single(t => t.Key == CertificatesMetric.Tags.ChainLevel).Value,
53+
CertificatesMetric.ChainLevels.All
54+
);
55+
Assert.Equal(
56+
CertificatesMetric.ValidityPeriods.Expired,
57+
m.Tags.Single(t => t.Key == CertificatesMetric.Tags.ValidityPeriod).Value);
58+
59+
60+
return true;
61+
});
62+
}
63+
[Fact]
64+
public void When_Ok() {
65+
66+
67+
var b = new CertificateProviderTest(_now.AddDays(-10), _now.AddDays(10));
68+
var sut = new CertificatesMetric(_meter, "cert_will_be_apocalypse", b);
69+
70+
71+
sut.Measure();
72+
var measurements = _doubleListener.RetrieveMeasurements("cert_will_be_apocalypse-day");
73+
74+
Assert.Contains(
75+
measurements,
76+
m => {
77+
78+
Assert.True(Math.Abs(m.Value -10)< 0.01);
79+
80+
81+
Assert.Equal(CertificatesMetric.Tags.All, m.Tags.Select(t => t.Key));
82+
Assert.Contains(
83+
m.Tags.Single(t => t.Key == CertificatesMetric.Tags.ChainLevel).Value,
84+
CertificatesMetric.ChainLevels.All
85+
);
86+
Assert.Equal(
87+
CertificatesMetric.ValidityPeriods.InPeriod,
88+
m.Tags.Single(t => t.Key == CertificatesMetric.Tags.ValidityPeriod).Value);
89+
90+
return true;
91+
});
92+
}
93+
94+
[Fact]
95+
public void When_Not_Yet_Valid() {
96+
97+
98+
var b = new CertificateProviderTest(_now.AddDays(10), _now.AddDays(11));
99+
var sut = new CertificatesMetric(_meter, "cert_future_apocalypse", b);
100+
101+
102+
sut.Measure();
103+
var measurements = _doubleListener.RetrieveMeasurements("cert_future_apocalypse-day");
104+
105+
Assert.Contains(
106+
measurements,
107+
m => {
108+
109+
Assert.True(Math.Abs(m.Value -11 ) < 0.01);
110+
111+
Assert.Equal(CertificatesMetric.Tags.All, m.Tags.Select(t => t.Key));
112+
Assert.Contains(
113+
m.Tags.Single(t => t.Key == CertificatesMetric.Tags.ChainLevel).Value,
114+
CertificatesMetric.ChainLevels.All
115+
);
116+
Assert.Equal(
117+
CertificatesMetric.ValidityPeriods.NotValidYet,
118+
m.Tags.Single(t => t.Key == CertificatesMetric.Tags.ValidityPeriod).Value);
119+
return true;
120+
});
121+
}
122+
123+
124+
public class CertificateProviderTest : CertificateProvider {
125+
126+
127+
public CertificateProviderTest(DateTimeOffset notBefore, DateTimeOffset notAfter) {
128+
using RSA rsa = RSA.Create();
129+
var request = new CertificateRequest("CN=SelfSignedCert", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
130+
131+
132+
this.Certificate=request.CreateSelfSigned(notBefore, notAfter);
133+
this.IntermediateCerts = new X509Certificate2Collection(
134+
135+
request.CreateSelfSigned(notBefore, notAfter)
136+
);
137+
this.TrustedRootCerts = new X509Certificate2Collection(request.CreateSelfSigned(notBefore, notAfter));
138+
}
139+
140+
public override LoadCertificateResult LoadCertificates(ClusterVNodeOptions options) => throw new NotImplementedException();
141+
142+
public override string GetReservedNodeCommonName() => throw new NotImplementedException();
143+
}
144+
145+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) Kurrent, Inc and/or licensed to Kurrent, Inc under one or more agreements.
2+
// Kurrent, Inc licenses this file to you under the Kurrent License v1 (see LICENSE.md).
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics.Metrics;
7+
using System.Security.Cryptography.X509Certificates;
8+
using KurrentDB.Core.Certificates;
9+
using KurrentDB.Core.Time;
10+
11+
namespace KurrentDB.Core.Metrics;
12+
13+
public class CertificatesMetric {
14+
private readonly Meter _meter;
15+
private readonly string _name;
16+
private readonly CertificateProvider _certificateProvider;
17+
private readonly List<CertificateExpiration> _subMetric= [];
18+
private readonly IClock _clock;
19+
20+
public static class ChainLevels {
21+
public const string Node="node";
22+
public const string Intermediate = "intermediate";
23+
public const string Root = "root";
24+
public static readonly string[] All = [Node, Intermediate, Root];
25+
}
26+
public static class Tags {
27+
public const string ChainLevel = "chain_level";
28+
public const string SerialNumber = "serial_number";
29+
public const string Thumbprint = "thumbprint";
30+
public const string ValidityPeriod = "validity_period";
31+
public static readonly string[] All = [ChainLevel, SerialNumber, Thumbprint, ValidityPeriod];
32+
}
33+
public static class ValidityPeriods {
34+
public const string InPeriod="0";
35+
public const string NotValidYet = "1";
36+
public const string Expired = "-1";
37+
}
38+
39+
40+
41+
public CertificatesMetric(Meter meter, string name,
42+
CertificateProvider certificateProvider,
43+
IClock clock = null) {
44+
_meter = meter;
45+
_name = name;
46+
_certificateProvider = certificateProvider;
47+
_clock = clock ?? Clock.Instance;
48+
49+
MeasureAllCerts(meter, name, certificateProvider);
50+
}
51+
52+
public void Measure() {
53+
foreach (var subMetric in _subMetric)
54+
subMetric.Measure();
55+
}
56+
57+
public void Renewed() => MeasureAllCerts(_meter, _name, _certificateProvider);
58+
59+
private void MeasureAllCerts(Meter meter, string name, CertificateProvider certificateProvider)
60+
{
61+
_subMetric.Clear();
62+
_subMetric.Add(new CertificateExpiration(certificateProvider.Certificate, ChainLevels.Node, meter, name,_clock));
63+
foreach (var intermediate in certificateProvider.IntermediateCerts)
64+
_subMetric.Add(new CertificateExpiration(intermediate, ChainLevels.Intermediate, meter, name,_clock));
65+
66+
foreach (var root in certificateProvider.TrustedRootCerts)
67+
_subMetric.Add(new CertificateExpiration(root, ChainLevels.Root, meter, name,_clock));
68+
}
69+
70+
71+
public class CertificateExpiration(X509Certificate2 cert, string type, Meter meter, string name, IClock clock ) {
72+
73+
private readonly Gauge<double> _gauge = meter.CreateGauge<double>(name, "day", "Days before the certificate expires");
74+
75+
private readonly KeyValuePair<string, object>[] _tags = [
76+
new(Tags.ChainLevel, type),
77+
new(Tags.SerialNumber, cert.GetSerialNumberString()),
78+
new(Tags.Thumbprint, cert.Thumbprint),
79+
new(Tags.ValidityPeriod, ValidityPeriod(DateTimeOffset.FromUnixTimeSeconds(clock.SecondsSinceEpoch).DateTime, cert.NotBefore, cert.NotAfter).ToString())
80+
81+
82+
];
83+
84+
private static string ValidityPeriod(DateTime now, DateTime notBefore, DateTime notAfter) =>
85+
notBefore <= now && now <= notAfter
86+
? ValidityPeriods.InPeriod
87+
: now <= notBefore
88+
? ValidityPeriods.NotValidYet
89+
: ValidityPeriods.Expired;
90+
91+
public void Measure() {
92+
var certApocalypseIn = (cert.NotAfter - DateTime.Now).TotalDays;
93+
_gauge.Record(certApocalypseIn, _tags);
94+
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)