forked from CESNET/rad_eap_test
-
Notifications
You must be signed in to change notification settings - Fork 0
/
rad_eap_test
executable file
·1031 lines (889 loc) · 38.5 KB
/
rad_eap_test
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
#!/bin/bash
# ===========================================================================================
# rad_eapol_test nagios compatible wrapper around eapol_test
# Copyright (c) 2005-2019 CESNET, z.s.p.o.
# Authors: Pavel Poláček <[email protected]>
# Jan Tomášek <[email protected]>
# Václav Mach <[email protected]>
# and others
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# See README and COPYING for more details.
# ===========================================================================================
# ===========================================================================================
# test certificate expiry based on user set number of days
# ===========================================================================================
function test_cert_expiry()
{
local not_after
local expiry_now
local expiry_warn
local expiry_date
# certicite expiry was set
if [[ -n "$CERTIFICATE_EXPIRY" ]]
then
process_cert_expiry # get the cert info needed
if [[ $? -ne 0 ]] # no cert available
then
return
fi
expiry_now=$(date +%s) # current date in seconds
expiry_warn=$(($expiry_now + ($CERTIFICATE_EXPIRY * 86400))) # when to warn before expiry in seconds
not_after=$(echo -e "$CERT" | openssl x509 -noout -dates | tail -1 | cut -d "=" -f 2) # get the "not after" date from cert
expiry_date=$(date -d "$not_after" "+%s") # get the expiry date in seconds
# do the actual expiry testing
if [[ $expiry_date -lt $expiry_now ]]
then
PROG_OUT=$(printf "CRITICAL: certificate EXPIRED %s\n" $(date -d "@$expiry_date" -Idate))
EXIT_CODE=$EXIT_CRITICAL
elif [[ $expiry_date -lt $expiry_warn ]]
then
PROG_OUT=$(printf "WARNING: cerificate expires soon (%s)\n" $(date -d "@$expiry_date" -Idate))
EXIT_CODE=$EXIT_WARNING
fi
fi
}
# ===========================================================================================
# add verbose info based on user set level
# ===========================================================================================
verbose()
{
# further processing based on VERBOSE level
case "$VERBOSE" in
0) ;; # nothing to do here, just an empty branch
# Show received Chargeable-User-Identity and/or Operator-Name
1)
echo ""
echo "$OUT" | sed -n '/(Access-Accept)/,$p' | awk '/Attribute (89|126) / { a=$3 } /Value: / && a { print a " " $2; a="" }' # get Attribute 89 or 126 and print the value
;;
# print the last packet decoded
2)
echo ""
echo "$OUT" | sed -n '/(Access-Accept)/,$p' # print from Access-Accept to the end
;;
# print all the packets decoded
3)
echo ""
echo "$OUT" | awk '/RADIUS message/ {print} /Attribute/ {print} /Value/ {print}'
;;
# print the raw output of eapol_test
4)
echo ""
echo "$OUT"
;;
esac
process_cert # also process certificates if wanted
}
# ===========================================================================================
# extract the certificate directly from eapol_test output
# params:
# 1) file where to write the cert
#
# there are some cases, when certificate is not written to file by eapol_test, for example:
# - when checking cert expiry there is a possibility that the server certificate already
# expired and eapol_test failed to successfully authenticate based on some configuration options
# When this happens eapol_test does not write the server cert (so it can be checked for expiry),
# so we need to check eapol_test output directly
# - when CA cert mismatch happens, no certificate is written to eapo_test output file
# ===========================================================================================
function extract_server_cert()
{
local hex
local certs
local cert_len
local pos=0
local header
hex=$(echo "$OUT" | grep -A 1 '(handshake/certificate)' | head -2 | tail -1 | # get the server cert hexdump message
cut -d ":" -f 3 | tr -d " " | # hex bytes
tail -c +21) # all the certs that the server sent in hex. tail strips first 20 bytes (determined by experiment) which is probably some openssl header
if [[ -z "$hex" ]]
then
return # no handshake/certificate string found in output
fi
while :
do
# there may be some EOC bytes after each processed cert
# to overcome this, read 1 byte at a time and check if it matches cert header
while [[ $pos -le ${#hex} ]] # reached end of hex string, end cert processing
do
header=${hex:$pos:4} # extract 2 byte cert header
if [[ $header != "3082" ]] # header does not match hex bytes 3082
then
((pos+=2)) # extract next byte
continue
else
break # header found
fi
done
if [[ $header != "3082" && $pos -gt ${#hex} ]] # header does not match hex bytes 3082 and pos is out of string
then
break # most likely some error
fi
((pos+=4)) # set pos for extracting cert_len
cert_len=${hex:$pos:4} # extract 2 byte cert length
cert_len=$((0x$cert_len)) # convert to decimal
((pos-=4)) # set pos to beginning of the current cert
if [[ -n "$certs" ]]
then
# cert len must be multiplied by 2 to get byte count, add header and cert len
certs="$certs\n$(echo "${hex:$pos:$cert_len*2 + 8}" | xxd -r -p | openssl x509 -inform der)" # extract just the cert bytes and pass it to openssl
else
# cert len must be multiplied by 2 to get byte count, add header and cert len
certs="$(echo "${hex:$pos:$cert_len*2 + 8}" | xxd -r -p | openssl x509 -inform der)" # extract just the cert bytes and pass it to openssl
fi
((pos=$pos + $cert_len*2 + 8)) # set pos for processing next cert: header (2 bytes) + cert_len (2 bytes) + cert (*2 for bytes) + current pos
done
if [[ -n "$certs" ]]
then
echo -e "$certs" > "$1"
fi
}
# ===========================================================================================
# check if server certificate was requested and if it was retrieved
# ===========================================================================================
function save_server_cert()
{
if [[ -n "$CERT_LOCATION" ]] # cert file does not exist or is empty
then
extract_server_cert "$CERT_LOCATION"
fi
}
# ===========================================================================================
# process RADIUS certificate for expiry check
# ===========================================================================================
function process_cert_expiry()
{
if [[ -n "$CERT_LOCATION" ]]
then
if [[ ! -s "$CERT_LOCATION" ]] # cert file does not exist or is empty
then
PROG_OUT="CRITICAL: Certificate expiry check was requested, but the certificate was not retrieved." # probably timeout ?
EXIT_CODE=$EXIT_CRITICAL
return 1 # cert not retrieved
else
get_cert_info "$CERT_LOCATION"
fi
fi
return 0 # no error
}
# ===========================================================================================
# process RADIUS certificate
# ===========================================================================================
function process_cert()
{
if [[ -n "$GET_CERT" ]] # print cert only if it was requested (-X or -B does not imply -b)
then
if [[ ! -s "$CERT_LOCATION" ]] # cert file does not exist or is empty
then
echo ""
echo "Certificate information was requested, but the certificate was not retrieved." # probably timeout ?
else
get_cert_info "$CERT_LOCATION"
print_cert
fi
fi
}
# ===========================================================================================
# print final output
# ===========================================================================================
function print_out()
{
echo "$PROG_OUT" # print the output
if [[ $EXIT_CODE -eq $EXIT_UNKNOWN ]]
then
: # do not add extra verbose output when exiting with unknown status
else
verbose # add extra verbose output if the user wants it, also process certs if wanted
fi
cleanup # cleanup temp files
}
# ===========================================================================================
# process results of the authentication and present them to the user
# ===========================================================================================
function process_auth_result()
{
EXIT_CODE=$EXIT_OK
# preset output needed for most situations
#
# FIX: decimal separator in bc(1) is '.' regardless of the locale
# rad_eap_test: Zeile 249: printf: .020428888: Ungültige Zahl.
# rad_eap_test: Zeile 250: printf: 20.428888000: Ungültige Zahl.
PROG_OUT=$(
# fake numeric locale just for printf with format %f
LC_ALL="C" printf "%s; %0.2f sec " "$STATUS_CODE" $TIME_SEC
LC_ALL="C" printf "|rtt=%0.0fms;;;0;%d accept=1;0.5:;0:;0;1\n" $TIME_MSEC $((TIMEOUT * 1000))
)
# processing based on $RETURN_CODE
case "$RETURN_CODE" in
$RET_SUCC) # successful authentication
;; # nothing to do here, EXIT_CODE is preset to 0
$RET_EAP_FAILED) # wrong username or password
EXIT_CODE=$EXIT_WARNING;;
$RET_RADIUS_NOT_AVAIL) # timeout
EXIT_CODE=$EXIT_CRITICAL;;
$RET_CERT_SUBJ_MISMATCH) # cert subject mismatch
EXIT_CODE=$EXIT_CRITICAL;;
$RET_CERT_CA_MISMATCH) # cert not matching specified CA
PROG_OUT=$(echo "$PROG_OUT" ; echo ""; get_ca_cert_mismatch_details ) # add extra error info about mismatch here
EXIT_CODE=$EXIT_CRITICAL;;
$RET_CERT_CA_MISMATCH_INCOMPLETE) # cert not matching specified CA, incomplete CA chain
EXIT_CODE=$EXIT_CRITICAL;;
$RET_CERT_EXPIRED) # cert expired
EXIT_CODE=$EXIT_CRITICAL;;
$RET_CERT_IN_FUTURE) # cert issued in future
EXIT_CODE=$EXIT_CRITICAL;;
$RET_DOMAIN_MISMATCH) # domain mismatch
EXIT_CODE=$EXIT_CRITICAL;;
$RET_PASSWD_EXPIRED) # MSCHAPv2 password expired
EXIT_CODE=$EXIT_WARNING;;
$RET_EAPOL_TEST_FAILED) # eapol_test return code was nonzero
PROG_OUT="eapol_test returned error: $OUT"
EXIT_CODE=$EXIT_UNKNOWN;;
*) # other case is probably error
PROG_OUT=$(echo "Probably configuration error, examine config in \"$MYTMPDIR\". Return code: " $RETURN_CODE)
EXIT_CODE=$EXIT_UNKNOWN;;
esac
test_cert_expiry # test cert expiry dates if requested, may override EXIT_CODE
print_out # print output, add verbose output if requested and cleanup
exit $EXIT_CODE # exit with $EXIT_CODE
}
# ===========================================================================================
# run eapol_test and try to authenticate using the specified configuration
# ===========================================================================================
function run_eapol_test()
{
BEGIN=$(date +%s.%N) # start the "timer"
# try to authenticate
OUT=$($EAPOL_PROG -c "$CONF" -a "$IP" -p "$PORT" -s "$SECRET" -t "$TIMEOUT" -M "$MAC" -C "$CONN_INFO" $EXTRA_EAPOL_ARGS 2>&1) # save output as a variable
EAPOL_PROG_RETCODE=$? # save the return code in case some error happened
END=$(date +%s.%N) # end the "timer"
}
# ===========================================================================================
# get details about domain name mismatch
# ===========================================================================================
function get_domain_mismatch_details()
{
# simple parsing of important info using awk
echo "$OUT" | awk '
BEGIN { count = 0 }
/TLS: Match domain against/,/TLS: Domain match/ { # everything between the two strings
if(match($0, /^TLS: Match domain against.*/)) # ignore lines starting with "TLS: Match domain against."
next
if(match($0, /^TLS: None of the dNSName\(s\) matched.*/)) # ignore lines starting with "TLS: None of the dNSName(s) matched."
next
if(match($0, /^TLS: No CommonName match found*/)) # ignore lines starting with "TLS: No CommonName match found"
next
if((match($0, /^TLS: Certificate dNSName.*/) && count == 0) || (match($0, /^TLS: Certificate commonName.*/) && count == 0)) { # first occurrence of domain name or common name, ignore it
count++
next
}
if(match($0, /^TLS: Certificate dNSName.*/) || match($0, /TLS: Certificate commonName.*/)) { # other dns names or common names, print comma after every domain name
printf(", ")
next
}
if(match($0, /^TLS: Domain match.*/)) {
printf(" not matching %s", $4) # print the requested match
exit(0) # end the program here
}
printf("%s", $NF) # print $NF
}
'
}
# ===========================================================================================
# get details about CA cert mismatch
# use just CN from certs
# ===========================================================================================
function get_ca_cert_mismatch_details()
{
echo -n "'$(openssl x509 -nameopt utf8 -in "$CA_CRT" -noout -subject)'"
if [[ -n "$CERT_LOCATION" && -s "$CERT_LOCATION" ]]
then
echo -n " is not matching '$(openssl x509 -nameopt utf8 -in "$CERT_LOCATION" -noout -issuer | tr -d "\n")'"
fi
}
# ===========================================================================================
# determine the return code of this program based on processing the eapol_test output
# ===========================================================================================
function determine_return_code()
{
# constants which define return codes based on eapol_test output
local eap_fail1='CTRL-EVENT-EAP-FAILURE EAP authentication failed'
local eap_fail2='EAP: Received EAP-Failure'
local timeout='EAPOL test timed out'
local succ1='SUCCESS'
local succ2='CTRL-EVENT-EAP-SUCCESS EAP authentication completed successfully'
local reject='Access-Reject'
local cert_subj_mismatch="err='Subject mismatch'"
local passwd_expired='EAP-MSCHAPV2: Password expired'
# 2 X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT: unable to get issuer certificate
# the issuer certificate of a looked up certificate could not be found. This normally means the list of trusted certificates is not complete.
local ca_mismatch_incomplete_chain="err='unable to get issuer certificate'" # certificate not matching specified CA. Specified CA does not have full valid chain (missing root or intermediate certs).
# from openssl man pages: 20 X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: unable to get local issuer certificate
# the issuer certificate could not be found: this occurs if the issuer certificate of an untrusted certificate cannot be found.
local ca_mismatch_cert_not_matching_ca="err='unable to get local issuer certificate'" # certificate not matching specified CA. Server cert does not match specified CA (different CA subject and issuer in server cert)
local ca_mismatch_selfsign="err='self signed certificate in certificate chain'" # certificate not matching specified CA. Server is sending full chain with root which is self-signed
local cert_expired="err='certificate has expired'"
local cert_not_yet_valid="err='certificate is not yet valid'"
local root_cert="depth=0" # CA certs are validated at depth 0 (root). TODO: What about intermediate certs?
local domain_mismatch="err='Domain mismatch'"
run_eapol_test
# check if there was an error launching eapol_test
if [[ $EAPOL_PROG_RETCODE -ne 0 ]]
then
RETURN_CODE=$RET_EAPOL_TEST_FAILED # eapol_test failed to execute
STATUS_CODE="$OUT"
fi
# determine the RETURN_CODE based on specific constants used in eapol_test output
if [[ "$OUT" =~ $succ1 || "$OUT" =~ $succ2 ]]
then
RETURN_CODE=$RET_SUCC # success
STATUS_CODE="access-accept"
elif [[ -n "$DOMAIN_MATCH" && "$OUT" =~ $domain_mismatch ]] # domain match was specified
then
RETURN_CODE=$RET_DOMAIN_MISMATCH # domain name mismatch
STATUS_CODE="access-reject (domain mismatch [$(get_domain_mismatch_details)])"
elif [[ -n "$SUBJ_MATCH" && "$OUT" =~ $cert_subj_mismatch ]] # certifitace subject was specified
then
RETURN_CODE=$RET_CERT_SUBJ_MISMATCH # certificate subject mismatch
STATUS_CODE="access-reject (certificate subject mismatch [$(echo "$OUT" | grep 'did not match with' | sed 's/TLS: Subject //')])"
elif [[ -n "$CA_CRT" && "$OUT" =~ $ca_mismatch_incomplete_chain ]] # CA was specified, but is incomplete
then
RETURN_CODE=$RET_CERT_CA_MISMATCH_INCOMPLETE # certificate not matching CA
STATUS_CODE="access-reject (certificate not matching specified CA [used CA does not have a complete chain])"
elif [[ -n "$CA_CRT" && "$OUT" =~ $ca_mismatch_cert_not_matching_ca ]] # CA was specified but does not match server cert
then
RETURN_CODE=$RET_CERT_CA_MISMATCH # certificate not matching CA
STATUS_CODE="access-reject (certificate not matching specified CA)"
elif [[ -n "$CA_CRT" && "$OUT" =~ $ca_mismatch_selfsign ]] # CA was specified but the server is sending a completely different chain
then
RETURN_CODE=$RET_CERT_CA_MISMATCH # certificate not matching CA
STATUS_CODE="access-reject (certificate not matching specified CA)"
elif [[ -n "$CA_CRT" && "$OUT" =~ $cert_expired && "(echo "$OUT" | grep "$cert_expired")" =~ $root_cert ]] # root CA certificate expired
then
RETURN_CODE=$RET_CERT_EXPIRED # certificate expired
STATUS_CODE="access-reject (CA certificate expired [$(echo "$OUT" | grep "$cert_expired" | sed 's/^.*subject/subject/; s/ err=.*//')])"
# certificate expired (a more complex logic would be probably needed to distinguish end server and intermediate certs)
elif [[ -n "$CA_CRT" && "$OUT" =~ $cert_expired && ! "(echo "$OUT" | grep "$cert_expired")" =~ $root_cert ]]
then
RETURN_CODE=$RET_CERT_EXPIRED # certificate expired
STATUS_CODE="access-reject (certificate expired [$(echo "$OUT" | grep "$cert_expired" | sed 's/^.*subject/subject/; s/ err=.*//')])"
# TODO - distinguish CA cert and server cert
elif [[ -n "$CA_CRT" && "$OUT" =~ $cert_not_yet_valid ]] # certificate is not yet valid
then
RETURN_CODE=$RET_CERT_IN_FUTURE # certificate issued in future
STATUS_CODE="access-reject (certificate is not yet valid [$(echo "$OUT" | grep "$cert_not_yet_valid" | sed 's/^.*subject/subject/; s/ err=.*//')])"
elif [[ "$OUT" =~ $passwd_expired ]] # MSCHAPv2 password expiry
then
RETURN_CODE=$RET_PASSWD_EXPIRED
STATUS_CODE="access-reject (password has expired)"
elif [[ "$OUT" =~ $eap_fail1 || "$OUT" =~ $eap_fail2 || "$OUT" =~ $reject ]]
then
RETURN_CODE=$RET_EAP_FAILED # auth failed
STATUS_CODE="access-reject"
elif [[ "$OUT" =~ $timeout ]]
then
RETURN_CODE=$RET_RADIUS_NOT_AVAIL # timeout ?
STATUS_CODE="timeout"
fi
save_server_cert # always save the server cert if the user requested it
TIME_SEC=$(echo "$END - $BEGIN" | bc) # how long the authentication took in seconds
TIME_MSEC=$(echo "$TIME_SEC * 1000" | bc) # how long the authentication took in milliseconds
}
# ===========================================================================================
# cleanup temporary files
# ===========================================================================================
function cleanup()
{
if [[ $CLEANUP -eq 1 ]]
then
rm -r $MYTMPDIR # delete all temp files
else
echo "$OUT" > $OUTFILE # write raw eapol_test output to temp file
echo "Leaving temporary files in $MYTMPDIR"
echo -e "\tConfiguration: $CONF"
echo -e "\tOutput: $OUTFILE"
echo -e "\tRADIUS certificate: $CERT_LOCATION"
fi
}
# ===========================================================================================
# print certificate information
# ===========================================================================================
function print_cert()
{
printf "\n"
printf "RADIUS server certificate information:\n"
printf "%s\n" "$(echo -e "$CERT" | openssl x509 -nameopt utf8 -noout -issuer -subject -dates | # extract issuer, subject, dates
sed 's/issuer=/Issuer: /; s/subject=/Subject: /; s/notBefore=/Validity\nNot Before: /; s/notAfter=/Not After: /')" # more readable output
printf "%s\n" "$(echo "$cert_info" | grep 'DNS:' | sed 's/^[[:space:]]*//g')" # DNS names cannot be extracted directly by openssl
}
# ===========================================================================================
# check that cert is end server cert - not root or intermediate cert
# params:
# 1) cert to check (as text)
# ===========================================================================================
function check_cert()
{
# we're only looking for cert that does not contain 'CA:TRUE' flag
res=$(echo -e "$1" | openssl x509 -text -noout | grep 'CA:TRUE')
if [[ -z "$res" ]]
then
return 0 # the cert is for end server
else
return 1 # intermediate or root
fi
}
# ===========================================================================================
# get RADIUS server certificate info
# params:
# 1) path to the cert
# ===========================================================================================
function get_cert_info()
{
in=false
if [[ -n "$cert_info" && -n "$CERT" ]] # both $CERT and $cert_info already set, no need to process certs again
then
return
fi
# read cert file line by line
while read line
do
if [[ "$in" = "true" ]]
then
CERT="$CERT\n$line"
fi
if [[ "$line" = "-----BEGIN CERTIFICATE-----" ]]
then
in=true
CERT="$line"
fi
if [[ "$line" = "-----END CERTIFICATE-----" ]]
then
in=false
check_cert "$CERT"
if [[ $? -eq 0 ]] # correct cert
then
cert_info=$(echo -e "$CERT" | openssl x509 -nameopt utf8 -text -noout)
break
fi
fi
done < "$1"
# no cert seemed correct as end server cert
# take the last one processed
if [[ -z "$cert_info" ]]
then
cert_info=$(echo -e "$CERT" | openssl x509 -nameopt utf8 -text -noout)
fi
}
# ===========================================================================================
# generate configuration for eapol_test
# ===========================================================================================
function generate_config()
{
echo "network={" > $CONF
echo " ssid=\"$SSID\"" >> $CONF
echo " key_mgmt=$METHOD" >> $CONF
echo " eap=$EAP" >> $CONF
if [[ "$EAP" = "PEAP" || "$EAP" = "TTLS" ]]
then
echo " pairwise=CCMP TKIP" >> $CONF
echo " group=CCMP TKIP WEP104 WEP40" >> $CONF
echo " phase2=\"auth=$PHASE2\"" >> $CONF
fi
if [[ ! -z "$CA_CRT" ]]
then
echo " ca_cert=\"$CA_CRT\"" >> $CONF
fi
echo " identity=\"$USERNAME\"" >> $CONF
if [[ ! -z "$ANONYM_ID" ]]
then
echo " anonymous_identity=\"$ANONYM_ID\"" >> $CONF
fi
if [[ "$EAP" = "TLS" ]]
then
echo " client_cert=\"$USER_CRT\"" >> $CONF
echo " private_key=\"$USER_KEY\"" >> $CONF
if [[ ! -z "$KEY_PASS" ]]
then
echo " private_key_passwd=\"$KEY_PASS\"" >> $CONF
fi
else
echo " password=\"$PASSWORD\"" >> $CONF
fi
if [[ -n "$SUBJ_MATCH" ]]
then
echo " subject_match=\"$SUBJ_MATCH\"" >> $CONF
fi
if [[ -n "$DOMAIN_MATCH" ]]
then
echo " domain_match=\"$DOMAIN_MATCH\"" >> $CONF
fi
echo "}" >> $CONF
}
# ===========================================================================================
# print usage for the program
# ===========================================================================================
function usage()
{
echo "# this program is a wrapper for eapol_test from wpa_supplicant project
# this script generates configuration for eapol_test and runs it
# eapol_test is a program for testing RADIUS protocol and EAP authentication methods
Parameters :
-H <address> - Address of RADIUS server (DNS name or IP address). When using DNS name IPv4 address will be used unless -6 option is present. Both IPv4 or IPv6 addresses may be used.
-P <port> - Port of RADIUS server
-S <secret> - Secret for RADIUS server communication
-u <username> - Username ([email protected])
-A <anonymous_id> - Anonymous identity ([email protected])
-p <password> - Password
-t <timeout> - Timeout (default is 5 seconds)
-m <method> - Method (WPA-EAP | IEEE8021X )
-v - Verbose (prints decoded last Access-accept packet)
-c - Prints all packets decoded
-s <ssid> - SSID
-e <method> - EAP method (PEAP | TLS | TTLS | LEAP)
-M <mac_addr> - MAC address in xx:xx:xx:xx:xx:xx format
-i <connect_info> - Connection info (in RADIUS log: connect from <connect_info>)
-d <domain_name> - Constraint for server domain name. FQDN is used as a full match requirement for the server certificate. Multiple values may be specified. Multiple values must be separated by semicollon.
-k <user_key_file> - user certificate key file
-l <user_key_file_password> - password for user certificate key file
-j <user_cert_file> - user certificate file
-a <ca_cert_file> - certificate of CA
-2 <phase2 method> - Phase2 type (PAP,CHAP,MSCHAPV2)
-x <subject_match> - Substring to be matched against the subject of the authentication server certificate.
-N - Identify and do not delete temporary files
-O <domain.edu.cctld> - Operator-Name value in domain name format
-I <ip address> - explicitly specify NAS-IP-Address
-C - request Chargeable-User-Identity
-E <vsa> - Vendor-Specific Attribute (VSA) in id:type:data format
-T - send Called-Station-Id in MAC:SSID format
-f - send big access-request to cause fragmentation
-b - print details about certificate of RADIUS server (whole certificate chain may be retrieved by eapol_test, there is a certain logic that tries to determine the end server cert and print it)
-B <file> - save certificate of RADIUS server to specified file
-n <directory> - store temporary files in specified directory
-g - print the entire unmodified output of eapol_test
-V - Show received Chargeable-User-Identity and/or Operator-Name
-X <warn_days> - check certificate expiry (whole certificate chain may be retrieved by eapol_test, there is a certain logic that tries to determine the end server cert which is checked for expiry)
-6 - force use of IPv6 when using DNS name as RADIUS server address
-4 - use IPv4 when using DNS name as RADIUS server address (this is the default, but the option exists for compatibility)
-h - show this message
" >&2
exit 1
}
# ===========================================================================================
# check configuration parameters, environment and various other things
# ===========================================================================================
function check_settings()
{
# check dependencies used in this script
if [[ -z "$(which bc)" ]]
then
echo "bc is required by rad_eap_test, please install if first."
return 1
fi
if [[ -z "$(which dig)" ]]
then
echo "dig is required by rad_eap_test, please install if first."
return 1
fi
if [[ -z "$(which sed)" ]]
then
echo "sed is required by rad_eap_test, please install if first."
return 1
fi
if [[ -z "$(which awk)" ]]
then
echo "awk is required by rad_eap_test, please install if first."
return 1
fi
# check if eapol_test exists
if [[ ! -e "$EAPOL_PROG" ]]
then
echo "eapol_test program \"$EAPOL_PROG\" not found"
return 1
fi
# check if eapol_test is executable
if [[ ! -x "$EAPOL_PROG" ]]
then
echo "eapol_test program \"$EAPOL_PROG\" is not executable"
return 1
fi
if [[ -z "$ADDRESS" ]]
then
echo "Address of RADIUS server is not specified. (option -H)"
return 1
fi
if [[ -z "$PORT" ]]
then
echo "Port of RADIUS server is not specified. (option -P)"
return 1
fi
if [[ -z "$SECRET" ]]
then
echo "Secret for RADIUS server communication is not specified. (option -S)"
return 1
fi
if [[ -z "$USERNAME" ]]
then
echo "Username is not specified. (option -u)"
return 1
fi
if [[ -z "$EAP" ]]
then
echo "EAP method is not specified. (option -e)"
return 1
fi
if [[ "$EAP" = "TLS" ]]
then
# we need certificate instead of password
if [[ -z "$USER_CRT" ]]
then
echo "User certificate file is not specified (EAP TLS method is used). (option -j)"
return 1
fi
if [[ ! -f "$USER_CRT" ]]
then
echo "User certificate file doesn't exist. (option -j)"
return 1
fi
if [[ -z "$USER_KEY" ]]
then
echo "User key file is not specified (EAP TLS method is used). (option -k)"
return 1
fi
if [[ ! -f "$USER_KEY" ]]
then
echo "User private key file doesn't exist. (option -k)"
return 1
fi
else # $EAP != "TLS"
if [[ -z "$PASSWORD" ]]
then
echo "Password is not specified. (option -p)"
return 1
fi
fi
if [[ -z "$METHOD" ]]
then
echo "Method is not specified. (option -m)"
return 1
fi
if [[ -n "$CA_CRT" && ! -f "$CA_CRT" ]]
then
echo "Certificate authority file doesn't exist. (option -a)";
return 1
fi
if [[ -z "$SSID" ]]
then
SSID="eduroam";
fi
if [[ -z "$PHASE2" ]]
then
PHASE2="MSCHAPV2"
fi
if [[ -n "$OPERATOR_NAME" ]]
then
# prefix the Operator_Name with NamespaceID value "1" (REALM) as per RFC5580
EXTRA_EAPOL_ARGS="$EXTRA_EAPOL_ARGS -N126:s:1$OPERATOR_NAME"
fi
if [[ -n "$NAS_IP_ADDRESS" ]]
then
NAS_IP_ADDRESS_HEX=$(printf '%02x%02x%02x%02x' $(echo "$NAS_IP_ADDRESS" | tr '.' ' ' ))
EXTRA_EAPOL_ARGS="$EXTRA_EAPOL_ARGS -N4:x:$NAS_IP_ADDRESS_HEX"
fi
if [[ -n "$REQUEST_CUI" ]]
then
EXTRA_EAPOL_ARGS="$EXTRA_EAPOL_ARGS -N89:x:00"
fi
if [[ -n "$VENDOR_SPECIFIC" ]]
then
local vendor_attr_value=$(
read vendor_id vendor_type vendor_data <<< $(echo "$VENDOR_SPECIFIC" | tr ':' ' ')
printf '%08x' $vendor_id
printf '%02x' $vendor_type
printf '%02x' $((2 + ${#vendor_data}))
echo -n $vendor_data | while read -n 1 character; do
printf '%02x' \'$character
done
)
EXTRA_EAPOL_ARGS="$EXTRA_EAPOL_ARGS -N26:x:$vendor_attr_value"
fi
if [[ -n "$FRAGMENT" ]]
then
for i in $(seq 1 6)
do
EXTRA_EAPOL_ARGS="$EXTRA_EAPOL_ARGS -N26:x:0000625A0BF961616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161"
done
fi
if [[ -n "$CALLED_STATION_ID" ]]
then
DASHEDMAC=$(echo "$MAC" | tr ':a-z' '-A-Z') # replace ':' with '-' and convert all lowercase to uppercase
EXTRA_EAPOL_ARGS="$EXTRA_EAPOL_ARGS -N30:s:$DASHEDMAC:$SSID"
fi
# address may be DNS name or an IPv4 address
# IPv4
IP=$(echo "$ADDRESS" | grep -P '^(\d{1,3}\.){3}\d{1,3}$')
# IPv6
if [[ -z "$IP" ]] # IPv6 regex taken from https://www.regextester.com/96774
then
IP=$(echo "$ADDRESS" | grep -P '^(?:(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-fA-F]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:(?:[0-9a-fA-F]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,1}(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:(?:[0-9a-fA-F]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,2}(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:(?:[0-9a-fA-F]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,3}(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:[0-9a-fA-F]{1,4})):)(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,4}(?:(?:[0-9a-fA-F]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,5}(?:(?:[0-9a-fA-F]{1,4})))?::)(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,6}(?:(?:[0-9a-fA-F]{1,4})))?::))))$')
fi
if [[ -z "$IP" ]]
then
if [[ "$IPV6" == "YES" ]]
then
IP=$(dig +short "$ADDRESS" AAAA)
else
IP=$(dig +short "$ADDRESS")
fi
fi
# Sanity check: did we get an IP address?
if [[ -z "$IP" ]]
then
echo "Hostname $ADDRESS could not be resolved to an IP address."
return 1
fi
# use specified directory for temp files if it was set
if [[ -z "$TMPDIR" ]]
then
MYTMPDIR=$(mktemp -d /tmp/rad_eap_test.XXXXXX)
else
MYTMPDIR=$(mktemp -d $TMPDIR/rad_eap_test.XXXXXX)
fi
# configuration files
CONF=$MYTMPDIR/tmp-$$.conf
OUTFILE=$MYTMPDIR/tmp-$$.out
# RADIUS server cert
if [[ -n "$WRITE_CERT" ]]
then
#EXTRA_EAPOL_ARGS="$EXTRA_EAPOL_ARGS -o $WRITE_CERT"
# eapol_test has some strange behavior (bugs?) which seriously affect it
# when using output file for writing server certs.
# This SHOULD be fixed in eapol_test code.
# Instead of using eapol_test to extract server certs,
# we rather implemented our own server certificate extraction directly from eapol_test output
CERT_LOCATION="$WRITE_CERT"
elif [[ -n "$GET_CERT" || -n "$CERTIFICATE_EXPIRY" ]]
then
#EXTRA_EAPOL_ARGS="$EXTRA_EAPOL_ARGS -o ${MYTMPDIR}/RADIUS_cert.pem"
CERT_LOCATION="${MYTMPDIR}/RADIUS_cert.pem"
fi
return 0
}
# ===========================================================================================
# process command line options and their arguments
# ===========================================================================================
function process_options()
{
while getopts "H:P:S:u:p:t:m:s:e:t:M:i:d:j:k:a:A:l:2:x:vcNO:I:CE:TfhbB:n:gVX:64" opt
do
case "$opt" in
H) ADDRESS=$OPTARG;;
P) PORT=$OPTARG;;
S) SECRET=$OPTARG;;
u) USERNAME=$OPTARG;;
p) PASSWORD=$OPTARG;;
t) TIMEOUT=$OPTARG;;
m) METHOD=$OPTARG;;
v) VERBOSE=2;;
c) VERBOSE=3;;
s) SSID=$OPTARG;;
e) EAP=$OPTARG;;
M) MAC=$OPTARG;;
i) CONN_INFO=$OPTARG;;
k) USER_KEY=$OPTARG;;
j) USER_CRT=$OPTARG;;
a) CA_CRT=$OPTARG;;
A) ANONYM_ID=$OPTARG;;
l) KEY_PASS=$OPTARG;;
2) PHASE2=$OPTARG;;
N) CLEANUP=0;;
x) SUBJ_MATCH=$OPTARG;;
O) OPERATOR_NAME=$OPTARG;;
I) NAS_IP_ADDRESS=$OPTARG;;
C) REQUEST_CUI="YES";;
E) VENDOR_SPECIFIC=$OPTARG;;
T) CALLED_STATION_ID="YES";;
f) FRAGMENT="YES";;
b) GET_CERT="YES";;
B) WRITE_CERT=$OPTARG;;
n) TMPDIR=$OPTARG;;
g) VERBOSE=4;;
V) VERBOSE=1;;
X) CERTIFICATE_EXPIRY=$OPTARG;;
d) DOMAIN_MATCH="$OPTARG";;
6) IPV6="YES";;
4) IPV6="NO";;
h) usage;;
\?) usage;;
esac
done
shift $((OPTIND-1))
}
# ===========================================================================================
# set the default configuration
# ===========================================================================================
function default_config()
{
# umask - make the files created readable only by the current user
umask 0077
# path to eapol_test
# try to determine the path automatically first
EAPOL_PROG=$(which eapol_test)
if [[ -z "$EAPOL_PROG" ]]
then
# manually set the path if it wasn't determined automatically
EAPOL_PROG=/usr/local/bin/eapol_test
fi
# default verbosity
VERBOSE=0
#default timeout
TIMEOUT=5
#default mac address
MAC="70:6f:6c:69:73:68"
# default connection info
CONN_INFO="rad_eap_test + eapol_test"
# return codes
RET_SUCC=3
RET_EAP_FAILED=4
RET_RADIUS_NOT_AVAIL=5
RET_CERT_SUBJ_MISMATCH=6
RET_CERT_CA_MISMATCH=7
RET_CERT_CA_MISMATCH_INCOMPLETE=8
RET_CERT_EXPIRED=9
RET_DOMAIN_MISMATCH=10
RET_EAPOL_TEST_FAILED=11
RET_CERT_IN_FUTURE=12
RET_PASSWD_EXPIRED=13
# exit codes
EXIT_OK=0
EXIT_WARNING=1