-
Notifications
You must be signed in to change notification settings - Fork 3
/
certs.go
1352 lines (1112 loc) · 41.2 KB
/
certs.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2020 Adam Chalkley
//
// https://github.com/atc0005/check-cert
//
// Licensed under the MIT License. See LICENSE file in the project root for
// full license information.
package certs
import (
"crypto"
// We use this to verify MD5WithRSA signatures.
// nolint:gosec
"crypto/md5"
"crypto/sha256"
// We use this to generate SHA1 fingerprints
// nolint:gosec
"crypto/sha1"
"crypto/sha512"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"math"
"math/big"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/atc0005/check-cert/internal/textutils"
"github.com/atc0005/go-nagios"
)
var (
// ErrMissingValue indicates that an expected value was missing.
ErrMissingValue = errors.New("missing expected value")
// ErrNoCertsFound indicates that no certificates were found when
// evaluating a certificate chain. This error is not really expected to
// ever occur.
ErrNoCertsFound = errors.New("no certificates found")
// ErrExpiredCertsFound indicates that one or more certificates were found
// to be expired when evaluating a certificate chain.
ErrExpiredCertsFound = errors.New("expired certificates found")
// ErrExpiringCertsFound indicates that one or more certificates were
// found to be expiring soon when evaluating a certificate chain.
ErrExpiringCertsFound = errors.New("expiring certificates found")
// ErrHostnameVerificationFailed indicates a mismatch between a
// certificate and a given hostname.
ErrHostnameVerificationFailed = errors.New("hostname verification failed")
// ErrCertMissingSANsEntries indicates that a certificate is missing one or
// more Subject Alternate Names specified by the user.
ErrCertMissingSANsEntries = errors.New("certificate is missing requested SANs entries")
// ErrCertMissingSANsEntries = errors.New("certificate is missing Subject Alternate Name entries")
// ErrCertHasUnexpectedSANsEntries indicates that a certificate has one or
// more Subject Alternate Names not specified by the user.
ErrCertHasUnexpectedSANsEntries = errors.New("certificate has unexpected SANs entries")
// ErrCertHasUnexpectedSANsEntries = errors.New("certificate has unexpected Subject Alternate Name entries")
// ErrCertHasMissingAndUnexpectedSANsEntries indicates that a certificate is
// missing one or more Subject Alternate Names specified by the user and also
// contains one more more Subject Alternate Names not specified by the user.
ErrCertHasMissingAndUnexpectedSANsEntries = errors.New("certificate is missing requested SANs entries, has unexpected SANs entries")
// ErrCertHasMissingAndUnexpectedSANsEntries = errors.New("certificate is missing and has unexpected Subject Alternate Name entries")
// ErrX509CertReliesOnCommonName mirrors the unexported error string
// emitted by the HostnameError.Error() method from the x509 package.
//
// https://cs.opensource.google/go/go/+/refs/tags/go1.20.1:src/crypto/x509/verify.go;l=104
//
// This error string is emitted when a certificate is missing Subject
// Alternate Names (SANs) AND a specified hostname matches the Common Name
// field.
//
// TODO: Open RFE in Go project asking that this be made an exportable
// error value so that we can drop this hard-coded version (which is bound
// to become a problem at some point).
// https://github.com/atc0005/check-cert/issues/520
//
ErrX509CertReliesOnCommonName = errors.New("x509: certificate relies on legacy Common Name field, use SANs instead")
// ErrNoCertValidationResults indicates that the cert chain validation
// results collection is empty. This is an unusual condition as
// configuration validation requires that at least one validation check is
// performed.
ErrNoCertValidationResults = errors.New("certificate validation results collection is empty")
)
// ServiceStater represents a type that is capable of evaluating its overall
// state.
type ServiceStater interface {
IsCriticalState() bool
IsWarningState() bool
IsOKState() bool
}
// CertChainValidationOptions is a collection of validation options shared by
// all validation functions for types implementing the
// CertChainValidationResult interface.
//
// Not all options are used by each validation function.
type CertChainValidationOptions struct {
// IgnoreHostnameVerificationFailureIfEmptySANsList tracks whether a
// request was made to ignore validation check results for the hostname
// when the leaf certificate's Subject Alternate Names (SANs) list is
// found to be empty.
IgnoreHostnameVerificationFailureIfEmptySANsList bool
// IgnoreValidationResultExpiration tracks whether a request was made to
// ignore validation check results for certificate expiration. This is a
// broad/blanket request that ignores expiration validation issues for ALL
// certificates in a chain, not just the leaf/server certificate.
IgnoreValidationResultExpiration bool
// IgnoreValidationResultHostname tracks whether a request was made to
// ignore validation check results from verifying a given hostname against
// the leaf certificate in a certificate chain.
IgnoreValidationResultHostname bool
// IgnoreValidationResultSANs tracks whether a request was made to ignore
// validation check results result from performing a Subject Alternate
// Names (SANs) validation against a leaf certificate in a chain.
IgnoreValidationResultSANs bool
// IgnoreExpiredIntermediateCertificates tracks whether a request was made
// to ignore validation check results for certificate expiration against
// intermediate certificates in a certificate chain.
IgnoreExpiredIntermediateCertificates bool
// IgnoreExpiredRootCertificates tracks whether a request was made to
// ignore validation check results for certificate expiration against root
// certificates in a certificate chain.
IgnoreExpiredRootCertificates bool
}
// DiscoveredCertChain represents the certificate chain found on a specific
// host along with that host's IP/Name and port.
type DiscoveredCertChain struct {
// Name is the hostname or FQDN of a system where a certificate chain was
// retrieved. Depending on how scan targets were specified, this value may
// not be populated.
Name string
// IPAddress is the IP Address where a certificate chain was discovered.
// This value should always be populated.
IPAddress string
// Port is the TCP port where a certificate chain was retrieved.
Port int
// Certs is the certificate chain associated with a host.
Certs []*x509.Certificate
}
// DiscoveredCertChains is a collection of discovered certificate chains for
// specified hosts and ports.
type DiscoveredCertChains []DiscoveredCertChain
// CertValidityDateLayout is the chosen date layout for displaying certificate
// validity date/time values across our application.
const CertValidityDateLayout string = "2006-01-02 15:04:05 -0700 MST"
const (
certChainPositionLeaf string = "leaf"
certChainPositionLeafSelfSigned string = "leaf; self-signed"
certChainPositionIntermediate string = "intermediate"
certChainPositionRoot string = "root"
certChainPositionUnknown string = "UNKNOWN cert chain position; please submit a bug report"
)
// ExpirationValidationOneLineSummaryExpiresNextTmpl is a shared template
// string used for emitting one-line service check status output for
// certificate chains whose certificates have not expired yet.
const ExpirationValidationOneLineSummaryExpiresNextTmpl string = "%s validation %s: %s cert %q expires next with %s (until %s)"
// ExpirationValidationOneLineSummaryExpiredTmpl is a shared template string
// used for emitting one-line service check status output for certificate
// chains with expired certificates.
const ExpirationValidationOneLineSummaryExpiredTmpl string = "%s validation %s: %s cert %q expired %s (on %s)"
// X509CertReliesOnCommonName mirrors the unexported error string emitted by
// the HostnameError.Error() method from the x509 package.
//
// This error string is emitted when a certificate is missing Subject
// Alternate Names (SANs) AND a specified hostname matches the Common Name
// field.
//
// Deprecated: See the ErrX509CertReliesOnCommonName value instead.
const X509CertReliesOnCommonName string = "x509: certificate relies on legacy Common Name field, use SANs instead"
// Names of certificate chain validation checks.
const (
// checkNameExpirationValidationResult string = "Certificate Chain Expiration"
// checkNameHostnameValidationResult string = "Leaf Certificate Hostname"
// checkNameSANsListValidationResult string = "Leaf Certificate SANs List"
// checkNameExpirationValidationResult string = "Expiration Validation"
// checkNameHostnameValidationResult string = "Hostname Validation"
// checkNameSANsListValidationResult string = "SANs List Validation"
checkNameExpirationValidationResult string = "Expiration"
checkNameHostnameValidationResult string = "Hostname"
checkNameSANsListValidationResult string = "SANs List"
)
// Baseline priority values for validation results. Higher values indicate
// higher priority.
const (
baselinePrioritySANsListValidationResult int = iota + 1
baselinePriorityHostnameValidationResult
baselinePriorityExpirationValidationResult
)
// Priority modifiers for validation results. These values are used to boost
// the baseline priority of a validation result in order to allow it to "jump
// the line" for review purposes.
const (
// priorityModifierMaximum represents the maximum priority modifier for a
// validation result. This modifier is usually applied for critical sanity
// check failures (e.g., wrong range of requested values) or significant
// issues needing immediate attention (e.g., "expired certificates" vs
// "expiring soon" certificates).
priorityModifierMaximum int = 999
// priorityModifierMedium represents a medium priority modifier for a
// validation result. This modifier is usually applied for check failures
// with optional workarounds (e.g., "empty SANs list on cert").
priorityModifierMedium int = 2
// priorityModifierMinimum represents the minimum priority modifier for a
// validation result. This modifier is usually applied for minor check
// failures (e.g., "expiring soon").
priorityModifierMinimum int = 1
// priorityModifierBaseline represents the baseline priority modifier for
// a validation result. This modifier is usually applied in order to
// explicitly communicate that the default or baseline value for a
// validation check is used (NOOP; e.g., for an OK result).
priorityModifierBaseline int = 0
)
// ServiceState accepts a type capable of evaluating its status and uses those
// results to map to a compatible ServiceState value.
func ServiceState(val ServiceStater) nagios.ServiceState {
var stateLabel string
var stateExitCode int
switch {
case val.IsCriticalState():
stateLabel = nagios.StateCRITICALLabel
stateExitCode = nagios.StateCRITICALExitCode
case val.IsWarningState():
stateLabel = nagios.StateWARNINGLabel
stateExitCode = nagios.StateWARNINGExitCode
case val.IsOKState():
stateLabel = nagios.StateOKLabel
stateExitCode = nagios.StateOKExitCode
default:
stateLabel = nagios.StateUNKNOWNLabel
stateExitCode = nagios.StateUNKNOWNExitCode
}
return nagios.ServiceState{
Label: stateLabel,
ExitCode: stateExitCode,
}
}
// GetCertsFromFile is a helper function for retrieving a certificate chain
// from a specified PEM formatted certificate file. An error is returned if
// the file cannot be decoded and parsed (e.g., empty file, not PEM
// formatted). Any leading non-PEM formatted data is skipped while any
// trailing non-PEM formatted data is returned for potential further
// evaluation.
func GetCertsFromFile(filename string) ([]*x509.Certificate, []byte, error) {
var certChain []*x509.Certificate
// Read in the entire PEM certificate file after first attempting to
// sanitize the input file variable contents.
pemData, err := os.ReadFile(filepath.Clean(filename))
if err != nil {
return nil, nil, err
}
// Grab the first PEM formatted block in our PEM cert file data.
block, rest := pem.Decode(pemData)
switch {
case block == nil:
return nil, nil, fmt.Errorf(
"failed to decode %s as PEM formatted certificate file; potentially malformed certificate",
filename,
)
case len(block.Bytes) == 0:
return nil, nil, fmt.Errorf(
"failed to decode %s as PEM formatted certificate file; potentially empty certificate file",
filename,
)
}
// If there is only one certificate (e.g., "server" or "leaf" certificate)
// we'll only get one block from the last pem.Decode() call. However, if
// the file contains a certificate chain or "bundle" we will need to call
// pem.Decode() multiple times, so we setup a loop to handle that.
for {
if block != nil {
// fmt.Println("Type of block:", block.Type)
// fmt.Println("size of file content:", len(pemData))
// fmt.Println("size of rest:", len(rest))
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, err
}
// we got a cert. Let's add it to our list
certChain = append(certChain, cert)
if len(rest) > 0 {
block, rest = pem.Decode(rest)
// if we were able to decode the "rest" of the data, then
// iterate again so we can parse it
if block != nil {
continue
}
}
break
}
// we're done attempting to decode the cert file; we have found data
// that fails to decode properly
if len(rest) > 0 {
break
}
}
return certChain, rest, err
}
// IsExpiredCert receives a x509 certificate and returns a boolean value
// indicating whether the cert has expired.
func IsExpiredCert(cert *x509.Certificate) bool {
return cert.NotAfter.Before(time.Now())
}
// IsExpiringCert receives a x509 certificate, CRITICAL age threshold and
// WARNING age threshold values and uses the provided thresholds to determine
// if the certificate is about to expire. A boolean value is returned to
// indicate the results of this check. An expired certificate fails this
// check.
func IsExpiringCert(cert *x509.Certificate, ageCritical time.Time, ageWarning time.Time) bool {
switch {
case !IsExpiredCert(cert) && cert.NotAfter.Before(ageCritical):
return true
case !IsExpiredCert(cert) && cert.NotAfter.Before(ageWarning):
return true
}
return false
}
// HasExpiredCert receives a slice of x509 certificates and indicates whether
// any of the certificates in the chain have expired.
func HasExpiredCert(certChain []*x509.Certificate) bool {
for idx := range certChain {
if certChain[idx].NotAfter.Before(time.Now()) {
return true
}
}
return false
}
// HasExpiringCert receives a slice of x509 certificates, CRITICAL age
// threshold and WARNING age threshold values and ignoring any certificates
// already expired, uses the provided thresholds to determine if any
// certificates are about to expire. A boolean value is returned to indicate
// the results of this check.
func HasExpiringCert(certChain []*x509.Certificate, ageCritical time.Time, ageWarning time.Time) bool {
for idx := range certChain {
switch {
case !IsExpiredCert(certChain[idx]) && certChain[idx].NotAfter.Before(ageCritical):
return true
case !IsExpiredCert(certChain[idx]) && certChain[idx].NotAfter.Before(ageWarning):
return true
}
}
return false
}
// NumExpiredCerts receives a slice of x509 certificates and returns a count
// of how many certificates have expired.
func NumExpiredCerts(certChain []*x509.Certificate) int {
var expiredCertsCount int
for idx := range certChain {
if certChain[idx].NotAfter.Before(time.Now()) {
expiredCertsCount++
}
}
return expiredCertsCount
}
// NumExpiringCerts receives a slice of x509 certificates, CRITICAL age threshold
// and WARNING age threshold values and ignoring any certificates already
// expired, uses the provided thresholds to determine if any certificates are
// about to expire. A count of expiring certificates is returned.
func NumExpiringCerts(certChain []*x509.Certificate, ageCritical time.Time, ageWarning time.Time) int {
var expiringCertsCount int
for idx := range certChain {
switch {
case !IsExpiredCert(certChain[idx]) && certChain[idx].NotAfter.Before(ageCritical):
expiringCertsCount++
case !IsExpiredCert(certChain[idx]) && certChain[idx].NotAfter.Before(ageWarning):
expiringCertsCount++
}
}
return expiringCertsCount
}
// IsLeafCert indicates whether a given certificate from a certificate chain
// is a leaf or server certificate.
func IsLeafCert(cert *x509.Certificate, certChain []*x509.Certificate) bool {
chainPos := ChainPosition(cert, certChain)
switch chainPos {
case certChainPositionLeaf:
return true
case certChainPositionLeafSelfSigned:
return true
default:
return false
}
}
// IsIntermediateCert indicates whether a given certificate from a certificate
// chain is an intermediate certificate.
func IsIntermediateCert(cert *x509.Certificate, certChain []*x509.Certificate) bool {
chainPos := ChainPosition(cert, certChain)
return chainPos == certChainPositionIntermediate
}
// IsRootCert indicates whether a given certificate from a certificate chain
// is a root certificate.
func IsRootCert(cert *x509.Certificate, certChain []*x509.Certificate) bool {
chainPos := ChainPosition(cert, certChain)
return chainPos == certChainPositionRoot
}
// NumLeafCerts receives a slice of x509 certificates and returns a count of
// leaf certificates present in the chain.
func NumLeafCerts(certChain []*x509.Certificate) int {
var num int
for _, cert := range certChain {
chainPos := ChainPosition(cert, certChain)
switch chainPos {
case certChainPositionLeaf:
num++
case certChainPositionLeafSelfSigned:
num++
}
}
return num
}
// NumIntermediateCerts receives a slice of x509 certificates and returns a
// count of intermediate certificates present in the chain.
func NumIntermediateCerts(certChain []*x509.Certificate) int {
var num int
for _, cert := range certChain {
chainPos := ChainPosition(cert, certChain)
if chainPos == certChainPositionIntermediate {
num++
}
}
return num
}
// NumRootCerts receives a slice of x509 certificates and returns a
// count of root certificates present in the chain.
func NumRootCerts(certChain []*x509.Certificate) int {
var num int
for _, cert := range certChain {
chainPos := ChainPosition(cert, certChain)
if chainPos == certChainPositionRoot {
num++
}
}
return num
}
// NumUnknownCerts receives a slice of x509 certificates and returns a count
// of unidentified certificates present in the chain.
func NumUnknownCerts(certChain []*x509.Certificate) int {
var num int
for _, cert := range certChain {
chainPos := ChainPosition(cert, certChain)
if chainPos == certChainPositionUnknown {
num++
}
}
return num
}
// LeafCerts receives a slice of x509 certificates and returns a (potentially
// empty) collection of leaf certificates present in the chain.
func LeafCerts(certChain []*x509.Certificate) []*x509.Certificate {
numPresent := NumLeafCerts(certChain)
leafCerts := make([]*x509.Certificate, 0, numPresent)
for _, cert := range certChain {
chainPos := ChainPosition(cert, certChain)
switch chainPos {
case certChainPositionLeaf:
leafCerts = append(leafCerts, cert)
case certChainPositionLeafSelfSigned:
leafCerts = append(leafCerts, cert)
}
}
return leafCerts
}
// IntermediateCerts receives a slice of x509 certificates and returns a
// (potentially empty) collection of intermediate certificates present in the
// chain.
func IntermediateCerts(certChain []*x509.Certificate) []*x509.Certificate {
numPresent := NumIntermediateCerts(certChain)
intermediateCerts := make([]*x509.Certificate, 0, numPresent)
for _, cert := range certChain {
chainPos := ChainPosition(cert, certChain)
if chainPos == certChainPositionIntermediate {
intermediateCerts = append(intermediateCerts, cert)
}
}
return intermediateCerts
}
// RootCerts receives a slice of x509 certificates and returns a (potentially
// empty) collection of root certificates present in the chain.
func RootCerts(certChain []*x509.Certificate) []*x509.Certificate {
numPresent := NumRootCerts(certChain)
rootCerts := make([]*x509.Certificate, 0, numPresent)
for _, cert := range certChain {
chainPos := ChainPosition(cert, certChain)
if chainPos == certChainPositionRoot {
rootCerts = append(rootCerts, cert)
}
}
return rootCerts
}
// OldestLeafCert returns the oldest leaf certificate in a given certificate
// chain. If a leaf certificate is not not present nil is returned.
func OldestLeafCert(certChain []*x509.Certificate) *x509.Certificate {
leafs := LeafCerts(certChain)
return NextToExpire(leafs, false)
}
// OldestIntermediateCert returns the oldest intermediate certificate in a
// given certificate chain. If a leaf certificate is not not present nil is
// returned.
func OldestIntermediateCert(certChain []*x509.Certificate) *x509.Certificate {
intermediates := IntermediateCerts(certChain)
return NextToExpire(intermediates, false)
}
// OldestRootCert returns the oldest root certificate in a given certificate
// chain. If a root certificate is not not present nil is returned.
func OldestRootCert(certChain []*x509.Certificate) *x509.Certificate {
roots := RootCerts(certChain)
return NextToExpire(roots, false)
}
// ExpiresInDays evaluates the given certificate and returns the number of
// days until the certificate expires. If already expired, a negative number
// is returned indicating how many days the certificate is past expiration.
//
// An error is returned if the pointer to the given certificate is nil.
func ExpiresInDays(cert *x509.Certificate) (int, error) {
if cert == nil {
return 0, fmt.Errorf(
"func ExpiresInDays: unable to determine expiration: %w",
ErrMissingValue,
)
}
timeRemaining := time.Until(cert.NotAfter).Hours()
// Toss remainder so that we only get the whole number of days
daysRemaining := int(math.Trunc(timeRemaining / 24))
return daysRemaining, nil
}
// MaxLifespan returns the maximum lifespan for a given certificate from the
// date it was issued until the time it is scheduled to expire.
func MaxLifespan(cert *x509.Certificate) (time.Duration, error) {
if cert == nil {
return 0, fmt.Errorf(
"func MaxLifespan: unable to determine expiration: %w",
ErrMissingValue,
)
}
maxCertLifespan := cert.NotAfter.Sub(cert.NotBefore)
return maxCertLifespan, nil
}
// MaxLifespanInDays returns the maximum lifespan in days for a given
// certificate from the date it was issued until the time it is scheduled to
// expire.
func MaxLifespanInDays(cert *x509.Certificate) (int, error) {
if cert == nil {
return 0, fmt.Errorf(
"func MaxLifespanInDays: unable to determine expiration: %w",
ErrMissingValue,
)
}
maxCertLifespan := cert.NotAfter.Sub(cert.NotBefore)
daysMaxLifespan := int(math.Trunc(maxCertLifespan.Hours() / 24))
return daysMaxLifespan, nil
}
// LifeRemainingPercentage returns the percentage of remaining time before a
// certificate expires.
func LifeRemainingPercentage(cert *x509.Certificate) (float64, error) {
if cert == nil {
return 0, fmt.Errorf(
"func LifeRemainingPercentage: unable to determine expiration: %w",
ErrMissingValue,
)
}
if IsExpiredCert(cert) {
return 0.0, nil
}
daysMaxLifespan, err := MaxLifespanInDays(cert)
if err != nil {
return 0, err
}
daysRemaining, err := ExpiresInDays(cert)
if err != nil {
return 0, err
}
certLifeRemainingPercentage := float64(daysRemaining) / float64(daysMaxLifespan) * 100
return certLifeRemainingPercentage, nil
}
// LifeRemainingPercentageTruncated returns the truncated percentage of
// remaining time before a certificate expires.
func LifeRemainingPercentageTruncated(cert *x509.Certificate) (int, error) {
if cert == nil {
return 0, fmt.Errorf(
"func LifeRemainingPercentageTruncated: unable to determine expiration: %w",
ErrMissingValue,
)
}
if IsExpiredCert(cert) {
return 0, nil
}
certLifeRemainingPercentage, err := LifeRemainingPercentage(cert)
if err != nil {
return 0, err
}
certLifespanRemainingTruncated := int(math.Trunc(certLifeRemainingPercentage))
return certLifespanRemainingTruncated, nil
}
// FormattedExpiration receives a Time value and converts it to a string
// representing the largest useful whole units of time in days and hours. For
// example, if a certificate has 1 year, 2 days and 3 hours remaining until
// expiration, this function will return the string '367d 3h remaining', but
// if only 3 hours remain then '3h remaining' will be returned. If a
// certificate has expired, the 'ago' suffix will be used instead. For
// example, if a certificate has expired 3 hours ago, '3h ago' will be
// returned.
func FormattedExpiration(expireTime time.Time) string {
// hoursRemaining := time.Until(certificate.NotAfter)/time.Hour)/24,
timeRemaining := time.Until(expireTime).Hours()
var certExpired bool
var formattedTimeRemainingStr string
var daysRemainingStr string
var hoursRemainingStr string
// Flip sign back to positive, note that cert is expired for later use
if timeRemaining < 0 {
certExpired = true
timeRemaining *= -1
}
// Toss remainder so that we only get the whole number of days
daysRemaining := math.Trunc(timeRemaining / 24)
if daysRemaining > 0 {
daysRemainingStr = fmt.Sprintf("%dd", int64(daysRemaining))
}
// Multiply the whole number of days by 24 to get the hours value, then
// subtract from the original number of hours until cert expiration to get
// the number of hours leftover from the days calculation.
hoursRemaining := math.Trunc(timeRemaining - (daysRemaining * 24))
hoursRemainingStr = fmt.Sprintf("%dh", int64(hoursRemaining))
// Only join days and hours remaining if there *are* days remaining.
switch {
case daysRemainingStr != "":
formattedTimeRemainingStr = strings.Join(
[]string{daysRemainingStr, hoursRemainingStr},
" ",
)
default:
formattedTimeRemainingStr = hoursRemainingStr
}
switch {
case !certExpired:
formattedTimeRemainingStr = strings.Join([]string{formattedTimeRemainingStr, "remaining"}, " ")
case certExpired:
formattedTimeRemainingStr = strings.Join([]string{formattedTimeRemainingStr, "ago"}, " ")
}
return formattedTimeRemainingStr
}
// FormatCertSerialNumber receives a certificate serial number in its native
// type and formats it in the text format used by OpenSSL (and many other
// tools).
//
// Example: DE:FD:50:2B:C5:7F:79:F4
func FormatCertSerialNumber(sn *big.Int) string {
// convert serial number from native *bit.Int format to a hex string
// snHexStr := sn.Text(16)
//
// use Sprintf hex formatting in order to retain leading zero (GH-114)
// credit: inspired by discussion on mozilla/tls-observatory#245
snHexStr := fmt.Sprintf("%X", sn.Bytes())
delimiterPosition := 2
delimiter := ":"
// ignore the leading negative sign if present
if sn.Sign() == -1 {
snHexStr = strings.TrimPrefix(snHexStr, "-")
}
formattedSerialNum := textutils.InsertDelimiter(snHexStr, delimiter, delimiterPosition)
formattedSerialNum = strings.ToUpper(formattedSerialNum)
// add back negative sign if originally present
if sn.Sign() == -1 {
return "-" + formattedSerialNum
}
return formattedSerialNum
}
// ExpirationStatus receives a certificate and the expiration threshold values
// for CRITICAL and WARNING states and returns a human-readable string
// indicating the overall status at a glance. If requested, an expired
// certificate is marked as ignored.
func ExpirationStatus(cert *x509.Certificate, ageCritical time.Time, ageWarning time.Time, ignoreExpired bool) string {
var expiresText string
certExpiration := cert.NotAfter
var lifeRemainingText string
if remaining, err := LifeRemainingPercentageTruncated(cert); err == nil {
lifeRemainingText = fmt.Sprintf(" (%d%%)", remaining)
}
switch {
case certExpiration.Before(time.Now()) && ignoreExpired:
expiresText = fmt.Sprintf(
"[EXPIRED, IGNORED] %s%s",
FormattedExpiration(certExpiration),
lifeRemainingText,
)
case certExpiration.Before(time.Now()):
expiresText = fmt.Sprintf(
"[EXPIRED] %s%s",
FormattedExpiration(certExpiration),
lifeRemainingText,
)
case certExpiration.Before(ageCritical):
expiresText = fmt.Sprintf(
"[%s] %s%s",
nagios.StateCRITICALLabel,
FormattedExpiration(certExpiration),
lifeRemainingText,
)
case certExpiration.Before(ageWarning):
expiresText = fmt.Sprintf(
"[%s] %s%s",
nagios.StateWARNINGLabel,
FormattedExpiration(certExpiration),
lifeRemainingText,
)
default:
expiresText = fmt.Sprintf(
"[%s] %s%s",
nagios.StateOKLabel,
FormattedExpiration(certExpiration),
lifeRemainingText,
)
}
return expiresText
}
// ShouldCertExpirationBeIgnored evaluates a given certificate, its
// certificate chain and the validation options specified and indicates
// whether the certificate should be ignored.
func ShouldCertExpirationBeIgnored(
cert *x509.Certificate,
certChain []*x509.Certificate,
validationOptions CertChainValidationOptions,
) bool {
if validationOptions.IgnoreValidationResultExpiration {
return true
}
if IsRootCert(cert, certChain) {
if IsExpiredCert(cert) &&
validationOptions.IgnoreExpiredRootCertificates {
return true
}
}
if IsIntermediateCert(cert, certChain) {
if IsExpiredCert(cert) &&
validationOptions.IgnoreExpiredIntermediateCertificates {
return true
}
}
return false
}
// isSelfSigned is a helper function that attempts to validate whether a given
// certificate is self-signed by asserting that its signature can be validated
// with its own public key. Any errors encountered during signature validation
// are assumed to be an indication that a certificate is not self-signed.
func isSelfSigned(cert *x509.Certificate) bool {
if cert.Issuer.String() == cert.Subject.String() {
sigVerifyErr := cert.CheckSignature(
cert.SignatureAlgorithm,
cert.RawTBSCertificate,
cert.Signature,
)
switch {
// examine signature verification errors
case errors.Is(sigVerifyErr, x509.InsecureAlgorithmError(cert.SignatureAlgorithm)):
// fmt.Println("errors.Is match")
// Handle MD5 signature verification ourselves since Go considers
// the MD5 algorithm to be insecure (rightly so).
if cert.SignatureAlgorithm == x509.MD5WithRSA {
// fmt.Println("SignatureAlgorithm match")
// nolint:gosec
h := md5.New()
if _, err := h.Write(cert.RawTBSCertificate); err != nil {
// fmt.Println(
// "isSelfSigned: failed to generate MD5 hash of RawTBSCertificate:",
// err,
// )
return false
}
hashedBytes := h.Sum(nil)
if pub, ok := cert.PublicKey.(*rsa.PublicKey); ok {
// fmt.Println("type assertion for rsa.PublicKey successful")
md5RSASigVerifyErr := rsa.VerifyPKCS1v15(
pub, crypto.MD5, hashedBytes, cert.Signature,
)
switch {
case md5RSASigVerifyErr != nil:
// fmt.Println(
// "isSelfSigned: failed to validate MD5WithRSA signature:",
// md5RSASigVerifyErr,
// )
return false
default:
// fmt.Println("MD5 signature verified")
return true
}
}
}
// TODO: Do we need to check this ourselves in Go 1.18?
// if cert.SignatureAlgorithm == x509.SHA1WithRSA {
// }
return false
// no problems verifying self-signed signature
case sigVerifyErr == nil:
return true
}
}
return false
}
// ChainPosition receives a cert and the cert chain that it belongs to and
// returns a string indicating what position or "role" it occupies in the
// certificate chain.
//
// https://en.wikipedia.org/wiki/X.509
// https://tools.ietf.org/html/rfc5280
func ChainPosition(cert *x509.Certificate, certChain []*x509.Certificate) string {
// We require a valid certificate chain. Fail if not provided.
if certChain == nil {
return certChainPositionUnknown
}
switch cert.Version {
// Because v1 and v2 certs lack the more descriptive "intention"
// fields of v3 certs, we are limited in what checks we can apply. We
// rely on a combination of self-signed and literal chain position to