PNG  IHDRX cHRMz&u0`:pQ<bKGD pHYsodtIME MeqIDATxw]Wug^Qd˶ 6`!N:!@xI~)%7%@Bh&`lnjVF29gΨ4E$|>cɚ{gk= %,a KX%,a KX%,a KX%,a KX%,a KX%,a KX%, b` ǟzeאfp]<!SJmɤY޲ڿ,%c ~ع9VH.!Ͳz&QynֺTkRR.BLHi٪:l;@(!MԴ=žI,:o&N'Kù\vRmJ雵֫AWic H@" !: Cé||]k-Ha oݜ:y F())u]aG7*JV@J415p=sZH!=!DRʯvɱh~V\}v/GKY$n]"X"}t@ xS76^[bw4dsce)2dU0 CkMa-U5tvLƀ~mlMwfGE/-]7XAƟ`׮g ewxwC4\[~7@O-Q( a*XGƒ{ ՟}$_y3tĐƤatgvێi|K=uVyrŲlLӪuܿzwk$m87k( `múcE)"@rK( z4$D; 2kW=Xb$V[Ru819קR~qloѱDyįݎ*mxw]y5e4K@ЃI0A D@"BDk_)N\8͜9dz"fK0zɿvM /.:2O{ Nb=M=7>??Zuo32 DLD@D| &+֎C #B8ַ`bOb $D#ͮҪtx]%`ES`Ru[=¾!@Od37LJ0!OIR4m]GZRJu$‡c=%~s@6SKy?CeIh:[vR@Lh | (BhAMy=݃  G"'wzn޺~8ԽSh ~T*A:xR[ܹ?X[uKL_=fDȊ؂p0}7=D$Ekq!/t.*2ʼnDbŞ}DijYaȲ(""6HA;:LzxQ‘(SQQ}*PL*fc\s `/d'QXW, e`#kPGZuŞuO{{wm[&NBTiiI0bukcA9<4@SӊH*؎4U/'2U5.(9JuDfrޱtycU%j(:RUbArLֺN)udA':uGQN"-"Is.*+k@ `Ojs@yU/ H:l;@yyTn}_yw!VkRJ4P)~y#)r,D =ě"Q]ci'%HI4ZL0"MJy 8A{ aN<8D"1#IJi >XjX֔#@>-{vN!8tRݻ^)N_╗FJEk]CT՟ YP:_|H1@ CBk]yKYp|og?*dGvzنzӴzjֺNkC~AbZƷ`.H)=!QͷVTT(| u78y֮}|[8-Vjp%2JPk[}ԉaH8Wpqhwr:vWª<}l77_~{s۴V+RCģ%WRZ\AqHifɤL36: #F:p]Bq/z{0CU6ݳEv_^k7'>sq*+kH%a`0ԣisqにtү04gVgW΂iJiS'3w.w}l6MC2uԯ|>JF5`fV5m`Y**Db1FKNttu]4ccsQNnex/87+}xaUW9y>ͯ骵G{䩓Գ3+vU}~jJ.NFRD7<aJDB1#ҳgSb,+CS?/ VG J?|?,2#M9}B)MiE+G`-wo߫V`fio(}S^4e~V4bHOYb"b#E)dda:'?}׮4繏`{7Z"uny-?ǹ;0MKx{:_pÚmFמ:F " .LFQLG)Q8qN q¯¯3wOvxDb\. BKD9_NN &L:4D{mm o^tֽ:q!ƥ}K+<"m78N< ywsard5+вz~mnG)=}lYݧNj'QJS{S :UYS-952?&O-:W}(!6Mk4+>A>j+i|<<|;ر^߉=HE|V#F)Emm#}/"y GII웻Jі94+v뾧xu~5C95~ūH>c@덉pʃ1/4-A2G%7>m;–Y,cyyaln" ?ƻ!ʪ<{~h~i y.zZB̃/,雋SiC/JFMmBH&&FAbϓO^tubbb_hZ{_QZ-sύodFgO(6]TJA˯#`۶ɟ( %$&+V'~hiYy>922 Wp74Zkq+Ovn錄c>8~GqܲcWꂎz@"1A.}T)uiW4="jJ2W7mU/N0gcqܗOO}?9/wìXžΏ0 >֩(V^Rh32!Hj5`;O28؇2#ݕf3 ?sJd8NJ@7O0 b־?lldщ̡&|9C.8RTWwxWy46ah嘦mh٤&l zCy!PY?: CJyв]dm4ǜҐR޻RլhX{FƯanшQI@x' ao(kUUuxW_Ñ줮[w8 FRJ(8˼)_mQ _!RJhm=!cVmm ?sFOnll6Qk}alY}; "baӌ~M0w,Ggw2W:G/k2%R,_=u`WU R.9T"v,<\Ik޽/2110Ӿxc0gyC&Ny޽JҢrV6N ``یeA16"J³+Rj*;BϜkZPJaÍ<Jyw:NP8/D$ 011z֊Ⱳ3ι֘k1V_"h!JPIΣ'ɜ* aEAd:ݺ>y<}Lp&PlRfTb1]o .2EW\ͮ]38؋rTJsǏP@芎sF\> P^+dYJLbJ C-xϐn> ι$nj,;Ǖa FU *择|h ~izť3ᤓ`K'-f tL7JK+vf2)V'-sFuB4i+m+@My=O҈0"|Yxoj,3]:cо3 $#uŘ%Y"y죯LebqtҢVzq¼X)~>4L׶m~[1_k?kxֺQ`\ |ٛY4Ѯr!)N9{56(iNq}O()Em]=F&u?$HypWUeB\k]JɩSع9 Zqg4ZĊo oMcjZBU]B\TUd34ݝ~:7ڶSUsB0Z3srx 7`:5xcx !qZA!;%͚7&P H<WL!džOb5kF)xor^aujƍ7 Ǡ8/p^(L>ὴ-B,{ۇWzֺ^k]3\EE@7>lYBȝR.oHnXO/}sB|.i@ɥDB4tcm,@ӣgdtJ!lH$_vN166L__'Z)y&kH;:,Y7=J 9cG) V\hjiE;gya~%ks_nC~Er er)muuMg2;֫R)Md) ,¶ 2-wr#F7<-BBn~_(o=KO㭇[Xv eN_SMgSҐ BS헃D%g_N:/pe -wkG*9yYSZS.9cREL !k}<4_Xs#FmҶ:7R$i,fi!~' # !6/S6y@kZkZcX)%5V4P]VGYq%H1!;e1MV<!ϐHO021Dp= HMs~~a)ަu7G^];git!Frl]H/L$=AeUvZE4P\.,xi {-~p?2b#amXAHq)MWǾI_r`S Hz&|{ +ʖ_= (YS(_g0a03M`I&'9vl?MM+m~}*xT۲(fY*V4x@29s{DaY"toGNTO+xCAO~4Ϳ;p`Ѫ:>Ҵ7K 3}+0 387x\)a"/E>qpWB=1 ¨"MP(\xp߫́A3+J] n[ʼnӼaTbZUWb={~2ooKױӰp(CS\S筐R*JغV&&"FA}J>G֐p1ٸbk7 ŘH$JoN <8s^yk_[;gy-;߉DV{c B yce% aJhDȶ 2IdйIB/^n0tNtџdcKj4϶v~- CBcgqx9= PJ) dMsjpYB] GD4RDWX +h{y`,3ꊕ$`zj*N^TP4L:Iz9~6s) Ga:?y*J~?OrMwP\](21sZUD ?ܟQ5Q%ggW6QdO+\@ ̪X'GxN @'4=ˋ+*VwN ne_|(/BDfj5(Dq<*tNt1х!MV.C0 32b#?n0pzj#!38}޴o1KovCJ`8ŗ_"]] rDUy޲@ Ȗ-;xџ'^Y`zEd?0„ DAL18IS]VGq\4o !swV7ˣι%4FѮ~}6)OgS[~Q vcYbL!wG3 7띸*E Pql8=jT\꘿I(z<[6OrR8ºC~ډ]=rNl[g|v TMTղb-o}OrP^Q]<98S¤!k)G(Vkwyqyr޽Nv`N/e p/~NAOk \I:G6]4+K;j$R:Mi #*[AȚT,ʰ,;N{HZTGMoּy) ]%dHء9Պ䠬|<45,\=[bƟ8QXeB3- &dҩ^{>/86bXmZ]]yޚN[(WAHL$YAgDKp=5GHjU&99v簪C0vygln*P)9^͞}lMuiH!̍#DoRBn9l@ xA/_v=ȺT{7Yt2N"4!YN`ae >Q<XMydEB`VU}u]嫇.%e^ánE87Mu\t`cP=AD/G)sI"@MP;)]%fH9'FNsj1pVhY&9=0pfuJ&gޤx+k:!r˭wkl03׼Ku C &ѓYt{.O.zҏ z}/tf_wEp2gvX)GN#I ݭ߽v/ .& и(ZF{e"=V!{zW`, ]+LGz"(UJp|j( #V4, 8B 0 9OkRrlɱl94)'VH9=9W|>PS['G(*I1==C<5"Pg+x'K5EMd؞Af8lG ?D FtoB[je?{k3zQ vZ;%Ɠ,]E>KZ+T/ EJxOZ1i #T<@ I}q9/t'zi(EMqw`mYkU6;[t4DPeckeM;H}_g pMww}k6#H㶏+b8雡Sxp)&C $@'b,fPߑt$RbJ'vznuS ~8='72_`{q纶|Q)Xk}cPz9p7O:'|G~8wx(a 0QCko|0ASD>Ip=4Q, d|F8RcU"/KM opKle M3#i0c%<7׿p&pZq[TR"BpqauIp$ 8~Ĩ!8Սx\ւdT>>Z40ks7 z2IQ}ItԀ<-%S⍤};zIb$I 5K}Q͙D8UguWE$Jh )cu4N tZl+[]M4k8֦Zeq֮M7uIqG 1==tLtR,ƜSrHYt&QP윯Lg' I,3@P'}'R˪e/%-Auv·ñ\> vDJzlӾNv5:|K/Jb6KI9)Zh*ZAi`?S {aiVDԲuy5W7pWeQJk֤#5&V<̺@/GH?^τZL|IJNvI:'P=Ϛt"¨=cud S Q.Ki0 !cJy;LJR;G{BJy޺[^8fK6)=yʊ+(k|&xQ2`L?Ȓ2@Mf 0C`6-%pKpm')c$׻K5[J*U[/#hH!6acB JA _|uMvDyk y)6OPYjœ50VT K}cǻP[ $:]4MEA.y)|B)cf-A?(e|lɉ#P9V)[9t.EiQPDѠ3ϴ;E:+Օ t ȥ~|_N2,ZJLt4! %ա]u {+=p.GhNcŞQI?Nd'yeh n7zi1DB)1S | S#ًZs2|Ɛy$F SxeX{7Vl.Src3E℃Q>b6G ўYCmtկ~=K0f(=LrAS GN'ɹ9<\!a`)֕y[uՍ[09` 9 +57ts6}b4{oqd+J5fa/,97J#6yν99mRWxJyѡyu_TJc`~W>l^q#Ts#2"nD1%fS)FU w{ܯ R{ ˎ󅃏џDsZSQS;LV;7 Od1&1n$ N /.q3~eNɪ]E#oM~}v֯FڦwyZ=<<>Xo稯lfMFV6p02|*=tV!c~]fa5Y^Q_WN|Vs 0ҘދU97OI'N2'8N֭fgg-}V%y]U4 峧p*91#9U kCac_AFңĪy뚇Y_AiuYyTTYЗ-(!JFLt›17uTozc. S;7A&&<ԋ5y;Ro+:' *eYJkWR[@F %SHWP 72k4 qLd'J "zB6{AC0ƁA6U.'F3:Ȅ(9ΜL;D]m8ڥ9}dU "v!;*13Rg^fJyShyy5auA?ɩGHRjo^]׽S)Fm\toy 4WQS@mE#%5ʈfFYDX ~D5Ϡ9tE9So_aU4?Ѽm%&c{n>.KW1Tlb}:j uGi(JgcYj0qn+>) %\!4{LaJso d||u//P_y7iRJ߬nHOy) l+@$($VFIQ9%EeKʈU. ia&FY̒mZ=)+qqoQn >L!qCiDB;Y<%} OgBxB!ØuG)WG9y(Ą{_yesuZmZZey'Wg#C~1Cev@0D $a@˲(.._GimA:uyw֬%;@!JkQVM_Ow:P.s\)ot- ˹"`B,e CRtaEUP<0'}r3[>?G8xU~Nqu;Wm8\RIkբ^5@k+5(By'L&'gBJ3ݶ!/㮻w҅ yqPWUg<e"Qy*167΃sJ\oz]T*UQ<\FԎ`HaNmڜ6DysCask8wP8y9``GJ9lF\G g's Nn͵MLN֪u$| /|7=]O)6s !ĴAKh]q_ap $HH'\1jB^s\|- W1:=6lJBqjY^LsPk""`]w)󭃈,(HC ?䔨Y$Sʣ{4Z+0NvQkhol6C.婧/u]FwiVjZka&%6\F*Ny#8O,22+|Db~d ~Çwc N:FuuCe&oZ(l;@ee-+Wn`44AMK➝2BRՈt7g*1gph9N) *"TF*R(#'88pm=}X]u[i7bEc|\~EMn}P瘊J)K.0i1M6=7'_\kaZ(Th{K*GJyytw"IO-PWJk)..axӝ47"89Cc7ĐBiZx 7m!fy|ϿF9CbȩV 9V-՛^pV̌ɄS#Bv4-@]Vxt-Z, &ֺ*diؠ2^VXbs֔Ìl.jQ]Y[47gj=幽ex)A0ip׳ W2[ᎇhuE^~q흙L} #-b۸oFJ_QP3r6jr+"nfzRJTUqoaۍ /$d8Mx'ݓ= OՃ| )$2mcM*cЙj}f };n YG w0Ia!1Q.oYfr]DyISaP}"dIӗթO67jqR ҊƐƈaɤGG|h;t]䗖oSv|iZqX)oalv;۩meEJ\!8=$4QU4Xo&VEĊ YS^E#d,yX_> ۘ-e\ "Wa6uLĜZi`aD9.% w~mB(02G[6y.773a7 /=o7D)$Z 66 $bY^\CuP. (x'"J60׿Y:Oi;F{w佩b+\Yi`TDWa~|VH)8q/=9!g߆2Y)?ND)%?Ǐ`k/sn:;O299yB=a[Ng 3˲N}vLNy;*?x?~L&=xyӴ~}q{qE*IQ^^ͧvü{Huu=R|>JyUlZV, B~/YF!Y\u_ݼF{_C)LD]m {H 0ihhadd nUkf3oٺCvE\)QJi+֥@tDJkB$1!Đr0XQ|q?d2) Ӣ_}qv-< FŊ߫%roppVBwü~JidY4:}L6M7f٬F "?71<2#?Jyy4뷢<_a7_=Q E=S1И/9{+93֮E{ǂw{))?maÆm(uLE#lïZ  ~d];+]h j?!|$F}*"4(v'8s<ŏUkm7^7no1w2ؗ}TrͿEk>p'8OB7d7R(A 9.*Mi^ͳ; eeUwS+C)uO@ =Sy]` }l8^ZzRXj[^iUɺ$tj))<sbDJfg=Pk_{xaKo1:-uyG0M ԃ\0Lvuy'ȱc2Ji AdyVgVh!{]/&}}ċJ#%d !+87<;qN޼Nفl|1N:8ya  8}k¾+-$4FiZYÔXk*I&'@iI99)HSh4+2G:tGhS^繿 Kتm0 вDk}֚+QT4;sC}rՅE,8CX-e~>G&'9xpW,%Fh,Ry56Y–hW-(v_,? ; qrBk4-V7HQ;ˇ^Gv1JVV%,ik;D_W!))+BoS4QsTM;gt+ndS-~:11Sgv!0qRVh!"Ȋ(̦Yl.]PQWgٳE'`%W1{ndΗBk|Ž7ʒR~,lnoa&:ü$ 3<a[CBݮwt"o\ePJ=Hz"_c^Z.#ˆ*x z̝grY]tdkP*:97YľXyBkD4N.C_[;F9`8& !AMO c `@BA& Ost\-\NX+Xp < !bj3C&QL+*&kAQ=04}cC!9~820G'PC9xa!w&bo_1 Sw"ܱ V )Yl3+ס2KoXOx]"`^WOy :3GO0g;%Yv㐫(R/r (s } u B &FeYZh0y> =2<Ϟc/ -u= c&׭,.0"g"7 6T!vl#sc>{u/Oh Bᾈ)۴74]x7 gMӒ"d]U)}" v4co[ ɡs 5Gg=XR14?5A}D "b{0$L .\4y{_fe:kVS\\O]c^W52LSBDM! C3Dhr̦RtArx4&agaN3Cf<Ԉp4~ B'"1@.b_/xQ} _߃҉/gٓ2Qkqp0շpZ2fԫYz< 4L.Cyυι1t@鎫Fe sYfsF}^ V}N<_`p)alٶ "(XEAVZ<)2},:Ir*#m_YӼ R%a||EƼIJ,,+f"96r/}0jE/)s)cjW#w'Sʯ5<66lj$a~3Kʛy 2:cZ:Yh))+a߭K::N,Q F'qB]={.]h85C9cr=}*rk?vwV렵ٸW Rs%}rNAkDv|uFLBkWY YkX מ|)1!$#3%y?pF<@<Rr0}: }\J [5FRxY<9"SQdE(Q*Qʻ)q1E0B_O24[U'],lOb ]~WjHޏTQ5Syu wq)xnw8~)c 쫬gٲߠ H% k5dƝk> kEj,0% b"vi2Wس_CuK)K{n|>t{P1򨾜j>'kEkƗBg*H%'_aY6Bn!TL&ɌOb{c`'d^{t\i^[uɐ[}q0lM˕G:‚4kb祔c^:?bpg… +37stH:0}en6x˟%/<]BL&* 5&fK9Mq)/iyqtA%kUe[ڛKN]Ě^,"`/ s[EQQm?|XJ߅92m]G.E΃ח U*Cn.j_)Tѧj̿30ڇ!A0=͜ar I3$C^-9#|pk!)?7.x9 @OO;WƝZBFU keZ75F6Tc6"ZȚs2y/1 ʵ:u4xa`C>6Rb/Yм)^=+~uRd`/|_8xbB0?Ft||Z\##|K 0>>zxv8۴吅q 8ĥ)"6>~\8:qM}#͚'ĉ#p\׶ l#bA?)|g g9|8jP(cr,BwV (WliVxxᡁ@0Okn;ɥh$_ckCgriv}>=wGzβ KkBɛ[˪ !J)h&k2%07δt}!d<9;I&0wV/ v 0<H}L&8ob%Hi|޶o&h1L|u֦y~󛱢8fٲUsւ)0oiFx2}X[zVYr_;N(w]_4B@OanC?gĦx>мgx>ΛToZoOMp>40>V Oy V9iq!4 LN,ˢu{jsz]|"R޻&'ƚ{53ўFu(<٪9:΋]B;)B>1::8;~)Yt|0(pw2N%&X,URBK)3\zz&}ax4;ǟ(tLNg{N|Ǽ\G#C9g$^\}p?556]/RP.90 k,U8/u776s ʪ_01چ|\N 0VV*3H鴃J7iI!wG_^ypl}r*jɤSR 5QN@ iZ#1ٰy;_\3\BQQ x:WJv츟ٯ$"@6 S#qe딇(/P( Dy~TOϻ<4:-+F`0||;Xl-"uw$Цi󼕝mKʩorz"mϺ$F:~E'ҐvD\y?Rr8_He@ e~O,T.(ފR*cY^m|cVR[8 JҡSm!ΆԨb)RHG{?MpqrmN>߶Y)\p,d#xۆWY*,l6]v0h15M˙MS8+EdI='LBJIH7_9{Caз*Lq,dt >+~ّeʏ?xԕ4bBAŚjﵫ!'\Ը$WNvKO}ӽmSşذqsOy?\[,d@'73'j%kOe`1.g2"e =YIzS2|zŐƄa\U,dP;jhhhaxǶ?КZ՚.q SE+XrbOu%\GتX(H,N^~]JyEZQKceTQ]VGYqnah;y$cQahT&QPZ*iZ8UQQM.qo/T\7X"u?Mttl2Xq(IoW{R^ ux*SYJ! 4S.Jy~ BROS[V|žKNɛP(L6V^|cR7i7nZW1Fd@ Ara{詑|(T*dN]Ko?s=@ |_EvF]׍kR)eBJc" MUUbY6`~V޴dJKß&~'d3i WWWWWW
Current Directory: /opt/saltstack/salt/lib/python3.10/site-packages/salt/modules
Viewing File: /opt/saltstack/salt/lib/python3.10/site-packages/salt/modules/tls.py
r""" A salt module for SSL/TLS. Can create a Certificate Authority (CA) or use Self-Signed certificates. :depends: PyOpenSSL Python module (0.10 or later, 0.14 or later for X509 extension support) :configuration: Add the following values in /etc/salt/minion for the CA module to function properly: .. code-block:: yaml ca.cert_base_path: '/etc/pki' CLI Example #1: Creating a CA, a server request and its signed certificate: .. code-block:: bash # salt-call tls.create_ca my_little \ days=5 \ CN='My Little CA' \ C=US \ ST=Utah \ L=Salt Lake City \ O=Saltstack \ emailAddress=pleasedontemail@example.com Created Private Key: "/etc/pki/my_little/my_little_ca_cert.key" Created CA "my_little_ca": "/etc/pki/my_little_ca/my_little_ca_cert.crt" # salt-call tls.create_csr my_little CN=www.example.com Created Private Key: "/etc/pki/my_little/certs/www.example.com.key Created CSR for "www.example.com": "/etc/pki/my_little/certs/www.example.com.csr" # salt-call tls.create_ca_signed_cert my_little CN=www.example.com Created Certificate for "www.example.com": /etc/pki/my_little/certs/www.example.com.crt" CLI Example #2: Creating a client request and its signed certificate .. code-block:: bash # salt-call tls.create_csr my_little CN=DBReplica_No.1 cert_type=client Created Private Key: "/etc/pki/my_little/certs//DBReplica_No.1.key" Created CSR for "DBReplica_No.1": "/etc/pki/my_little/certs/DBReplica_No.1.csr" # salt-call tls.create_ca_signed_cert my_little CN=DBReplica_No.1 Created Certificate for "DBReplica_No.1": "/etc/pki/my_little/certs/DBReplica_No.1.crt" CLI Example #3: Creating both a server and client req + cert for the same CN .. code-block:: bash # salt-call tls.create_csr my_little CN=MasterDBReplica_No.2 \ cert_type=client Created Private Key: "/etc/pki/my_little/certs/MasterDBReplica_No.2.key" Created CSR for "DBReplica_No.1": "/etc/pki/my_little/certs/MasterDBReplica_No.2.csr" # salt-call tls.create_ca_signed_cert my_little CN=MasterDBReplica_No.2 Created Certificate for "DBReplica_No.1": "/etc/pki/my_little/certs/DBReplica_No.1.crt" # salt-call tls.create_csr my_little CN=MasterDBReplica_No.2 \ cert_type=server Certificate "MasterDBReplica_No.2" already exists (doh!) # salt-call tls.create_csr my_little CN=MasterDBReplica_No.2 \ cert_type=server type_ext=True Created Private Key: "/etc/pki/my_little/certs/DBReplica_No.1_client.key" Created CSR for "DBReplica_No.1": "/etc/pki/my_little/certs/DBReplica_No.1_client.csr" # salt-call tls.create_ca_signed_cert my_little CN=MasterDBReplica_No.2 Certificate "MasterDBReplica_No.2" already exists (DOH!) # salt-call tls.create_ca_signed_cert my_little CN=MasterDBReplica_No.2 \ cert_type=server type_ext=True Created Certificate for "MasterDBReplica_No.2": "/etc/pki/my_little/certs/MasterDBReplica_No.2_server.crt" CLI Example #4: Create a server req + cert with non-CN filename for the cert .. code-block:: bash # salt-call tls.create_csr my_little CN=www.anothersometh.ing \ cert_type=server type_ext=True Created Private Key: "/etc/pki/my_little/certs/www.anothersometh.ing_server.key" Created CSR for "DBReplica_No.1": "/etc/pki/my_little/certs/www.anothersometh.ing_server.csr" # salt-call tls_create_ca_signed_cert my_little CN=www.anothersometh.ing \ cert_type=server cert_filename="something_completely_different" Created Certificate for "www.anothersometh.ing": /etc/pki/my_little/certs/something_completely_different.crt """ import binascii import calendar import logging import math import os import re import time from datetime import datetime import salt.utils.data import salt.utils.files import salt.utils.stringutils from salt.exceptions import CommandExecutionError from salt.utils.versions import Version # pylint: disable=C0103 HAS_SSL = False X509_EXT_ENABLED = True try: import OpenSSL HAS_SSL = True OpenSSL_version = Version(OpenSSL.__dict__.get("__version__", "0.0")) except ImportError: pass log = logging.getLogger(__name__) two_digit_year_fmt = "%y%m%d%H%M%SZ" four_digit_year_fmt = "%Y%m%d%H%M%SZ" def __virtual__(): """ Only load this module if the ca config options are set """ global X509_EXT_ENABLED if HAS_SSL and OpenSSL_version >= Version("0.10"): if OpenSSL_version < Version("0.14"): X509_EXT_ENABLED = False log.debug( "You should upgrade pyOpenSSL to at least 0.14.1 to " "enable the use of X509 extensions in the tls module" ) elif OpenSSL_version <= Version("0.15"): log.debug( "You should upgrade pyOpenSSL to at least 0.15.1 to " "enable the full use of X509 extensions in the tls module" ) # NOTE: Not having configured a cert path should not prevent this # module from loading as it provides methods to configure the path. return True else: X509_EXT_ENABLED = False return ( False, "PyOpenSSL version 0.10 or later must be installed " "before this module can be used.", ) def _microtime(): """ Return a Unix timestamp as a string of digits :return: """ val1, val2 = math.modf(time.time()) val2 = int(val2) return f"{val1:f}{val2}" def _context_or_config(key): """ Return the value corresponding to the key in __context__ or if not present, fallback to config.option. """ return __context__.get(key, __salt__["config.option"](key)) def cert_base_path(cacert_path=None): """ Return the base path for certs from CLI or from options cacert_path absolute path to ca certificates root directory CLI Example: .. code-block:: bash salt '*' tls.cert_base_path """ return ( cacert_path or _context_or_config("ca.contextual_cert_base_path") or _context_or_config("ca.cert_base_path") ) def _cert_base_path(cacert_path=None): """ Retrocompatible wrapper """ return cert_base_path(cacert_path) def set_ca_path(cacert_path): """ If wanted, store the aforementioned cacert_path in context to be used as the basepath for further operations CLI Example: .. code-block:: bash salt '*' tls.set_ca_path /etc/certs """ if cacert_path: __context__["ca.contextual_cert_base_path"] = cacert_path return cert_base_path() def _new_serial(ca_name): """ Return a serial number in hex using os.urandom() and a Unix timestamp in microseconds. ca_name name of the CA CN common name in the request """ hashnum = int( binascii.hexlify( b"_".join( ( salt.utils.stringutils.to_bytes(_microtime()), os.urandom(5), ) ) ), 16, ) log.debug("Hashnum: %s", hashnum) # record the hash somewhere cachedir = __opts__["cachedir"] log.debug("cachedir: %s", cachedir) serial_file = f"{cachedir}/{ca_name}.serial" if not os.path.exists(cachedir): os.makedirs(cachedir) if not os.path.exists(serial_file): mode = "w" else: mode = "a+" with salt.utils.files.fopen(serial_file, mode) as ofile: ofile.write(str(hashnum)) return hashnum def _four_digit_year_to_two_digit(datetimeObj): return datetimeObj.strftime(two_digit_year_fmt) def _get_basic_info(ca_name, cert, ca_dir=None): """ Get basic info to write out to the index.txt """ if ca_dir is None: ca_dir = f"{_cert_base_path()}/{ca_name}" index_file = f"{ca_dir}/index.txt" cert = _read_cert(cert) expire_date = _four_digit_year_to_two_digit(_get_expiration_date(cert)) serial_number = format(cert.get_serial_number(), "X") # gotta prepend a / subject = "/" # then we can add the rest of the subject subject += "/".join([f"{x}={y}" for x, y in cert.get_subject().get_components()]) subject += "\n" return (index_file, expire_date, serial_number, subject) def _write_cert_to_database(ca_name, cert, cacert_path=None, status="V"): """ write out the index.txt database file in the appropriate directory to track certificates ca_name name of the CA cert certificate to be recorded """ set_ca_path(cacert_path) ca_dir = f"{cert_base_path()}/{ca_name}" index_file, expire_date, serial_number, subject = _get_basic_info( ca_name, cert, ca_dir ) index_data = "{}\t{}\t\t{}\tunknown\t{}".format( status, expire_date, serial_number, subject ) with salt.utils.files.fopen(index_file, "a+") as ofile: ofile.write(salt.utils.stringutils.to_str(index_data)) def maybe_fix_ssl_version(ca_name, cacert_path=None, ca_filename=None): """ Check that the X509 version is correct (was incorrectly set in previous salt versions). This will fix the version if needed. ca_name ca authority name cacert_path absolute path to ca certificates root directory ca_filename alternative filename for the CA .. versionadded:: 2015.5.3 CLI Example: .. code-block:: bash salt '*' tls.maybe_fix_ssl_version test_ca /etc/certs """ set_ca_path(cacert_path) if not ca_filename: ca_filename = f"{ca_name}_ca_cert" certp = f"{cert_base_path()}/{ca_name}/{ca_filename}.crt" ca_keyp = f"{cert_base_path()}/{ca_name}/{ca_filename}.key" with salt.utils.files.fopen(certp) as fic: cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, fic.read()) if cert.get_version() == 3: log.info("Regenerating wrong x509 version for certificate %s", certp) with salt.utils.files.fopen(ca_keyp) as fic2: try: # try to determine the key bits key = OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, fic2.read() ) bits = key.bits() except Exception: # pylint: disable=broad-except bits = 2048 try: days = ( datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") - datetime.utcnow() ).days except (ValueError, TypeError): days = 365 subj = cert.get_subject() create_ca( ca_name, bits=bits, days=days, CN=subj.CN, C=subj.C, ST=subj.ST, L=subj.L, O=subj.O, OU=subj.OU, emailAddress=subj.emailAddress, fixmode=True, ) def ca_exists(ca_name, cacert_path=None, ca_filename=None): """ Verify whether a Certificate Authority (CA) already exists ca_name name of the CA cacert_path absolute path to ca certificates root directory ca_filename alternative filename for the CA .. versionadded:: 2015.5.3 CLI Example: .. code-block:: bash salt '*' tls.ca_exists test_ca /etc/certs """ set_ca_path(cacert_path) if not ca_filename: ca_filename = f"{ca_name}_ca_cert" certp = f"{cert_base_path()}/{ca_name}/{ca_filename}.crt" if os.path.exists(certp): maybe_fix_ssl_version(ca_name, cacert_path=cacert_path, ca_filename=ca_filename) return True return False def _ca_exists(ca_name, cacert_path=None): """Retrocompatible wrapper""" return ca_exists(ca_name, cacert_path) def get_ca(ca_name, as_text=False, cacert_path=None): """ Get the certificate path or content ca_name name of the CA as_text if true, return the certificate content instead of the path cacert_path absolute path to ca certificates root directory CLI Example: .. code-block:: bash salt '*' tls.get_ca test_ca as_text=False cacert_path=/etc/certs """ set_ca_path(cacert_path) certp = "{0}/{1}/{1}_ca_cert.crt".format(cert_base_path(), ca_name) if not os.path.exists(certp): raise ValueError(f"Certificate does not exist for {ca_name}") else: if as_text: with salt.utils.files.fopen(certp) as fic: certp = salt.utils.stringutils.to_unicode(fic.read()) return certp def get_ca_signed_cert( ca_name, CN="localhost", as_text=False, cacert_path=None, cert_filename=None ): """ Get the certificate path or content ca_name name of the CA CN common name of the certificate as_text if true, return the certificate content instead of the path cacert_path absolute path to certificates root directory cert_filename alternative filename for the certificate, useful when using special characters in the CN .. versionadded:: 2015.5.3 CLI Example: .. code-block:: bash salt '*' tls.get_ca_signed_cert test_ca CN=localhost as_text=False cacert_path=/etc/certs """ set_ca_path(cacert_path) if not cert_filename: cert_filename = CN certp = f"{cert_base_path()}/{ca_name}/certs/{cert_filename}.crt" if not os.path.exists(certp): raise ValueError(f"Certificate does not exists for {CN}") else: if as_text: with salt.utils.files.fopen(certp) as fic: certp = salt.utils.stringutils.to_unicode(fic.read()) return certp def get_ca_signed_key( ca_name, CN="localhost", as_text=False, cacert_path=None, key_filename=None ): """ Get the certificate path or content ca_name name of the CA CN common name of the certificate as_text if true, return the certificate content instead of the path cacert_path absolute path to certificates root directory key_filename alternative filename for the key, useful when using special characters .. versionadded:: 2015.5.3 in the CN CLI Example: .. code-block:: bash salt '*' tls.get_ca_signed_key \ test_ca CN=localhost \ as_text=False \ cacert_path=/etc/certs """ set_ca_path(cacert_path) if not key_filename: key_filename = CN keyp = f"{cert_base_path()}/{ca_name}/certs/{key_filename}.key" if not os.path.exists(keyp): raise ValueError(f"Certificate does not exists for {CN}") else: if as_text: with salt.utils.files.fopen(keyp) as fic: keyp = salt.utils.stringutils.to_unicode(fic.read()) return keyp def _read_cert(cert): if isinstance(cert, str): try: with salt.utils.files.fopen(cert) as rfh: return OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, rfh.read() ) except Exception: # pylint: disable=broad-except log.exception("Failed to read cert from path %s", cert) return None else: if not hasattr(cert, "get_notAfter"): log.error("%s is not a valid cert path/object", cert) return None else: return cert def validate(cert, ca_name, crl_file): """ .. versionadded:: 3000 Validate a certificate against a given CA/CRL. cert path to the certifiate PEM file or string ca_name name of the CA crl_file full path to the CRL file """ store = OpenSSL.crypto.X509Store() cert_obj = _read_cert(cert) if cert_obj is None: raise CommandExecutionError( f"Failed to read cert from {cert}, see log for details" ) ca_dir = f"{cert_base_path()}/{ca_name}" ca_cert = _read_cert(f"{ca_dir}/{ca_name}_ca_cert.crt") store.add_cert(ca_cert) # These flags tell OpenSSL to check the leaf as well as the # entire cert chain. X509StoreFlags = OpenSSL.crypto.X509StoreFlags store.set_flags(X509StoreFlags.CRL_CHECK | X509StoreFlags.CRL_CHECK_ALL) if crl_file is None: crl = OpenSSL.crypto.CRL() else: with salt.utils.files.fopen(crl_file) as fhr: crl = OpenSSL.crypto.load_crl(OpenSSL.crypto.FILETYPE_PEM, fhr.read()) store.add_crl(crl) context = OpenSSL.crypto.X509StoreContext(store, cert_obj) ret = {} try: context.verify_certificate() ret["valid"] = True except OpenSSL.crypto.X509StoreContextError as e: ret["error"] = str(e) ret["error_cert"] = e.certificate ret["valid"] = False return ret def _get_expiration_date(cert): """ Returns a datetime.datetime object """ cert_obj = _read_cert(cert) if cert_obj is None: raise CommandExecutionError( f"Failed to read cert from {cert}, see log for details" ) return datetime.strptime( salt.utils.stringutils.to_str(cert_obj.get_notAfter()), four_digit_year_fmt ) def get_expiration_date(cert, date_format="%Y-%m-%d"): """ .. versionadded:: 2019.2.0 Get a certificate's expiration date cert Full path to the certificate date_format By default this will return the expiration date in YYYY-MM-DD format, use this to specify a different strftime format string. Note that the expiration time will be in UTC. CLI Examples: .. code-block:: bash salt '*' tls.get_expiration_date /path/to/foo.crt salt '*' tls.get_expiration_date /path/to/foo.crt date_format='%d/%m/%Y' """ return _get_expiration_date(cert).strftime(date_format) def _check_onlyif_unless(onlyif, unless): ret = None retcode = __salt__["cmd.retcode"] if onlyif is not None: if not isinstance(onlyif, str): if not onlyif: ret = {"comment": "onlyif condition is false", "result": True} elif isinstance(onlyif, str): if retcode(onlyif) != 0: ret = {"comment": "onlyif condition is false", "result": True} log.debug("onlyif condition is false") if unless is not None: if not isinstance(unless, str): if unless: ret = {"comment": "unless condition is true", "result": True} elif isinstance(unless, str): if retcode(unless) == 0: ret = {"comment": "unless condition is true", "result": True} log.debug("unless condition is true") return ret def create_ca( ca_name, bits=2048, days=365, CN="localhost", C="US", ST="Utah", L="Salt Lake City", O="SaltStack", OU=None, emailAddress=None, fixmode=False, cacert_path=None, ca_filename=None, digest="sha256", onlyif=None, unless=None, replace=False, ): """ Create a Certificate Authority (CA) ca_name name of the CA bits number of RSA key bits, default is 2048 days number of days the CA will be valid, default is 365 CN common name in the request, default is "localhost" C country, default is "US" ST state, default is "Utah" L locality, default is "Centerville", the city where SaltStack originated O organization, default is "SaltStack" OU organizational unit, default is None emailAddress email address for the CA owner, default is None cacert_path absolute path to ca certificates root directory ca_filename alternative filename for the CA .. versionadded:: 2015.5.3 digest The message digest algorithm. Must be a string describing a digest algorithm supported by OpenSSL (by EVP_get_digestbyname, specifically). For example, "md5" or "sha1". Default: 'sha256' replace Replace this certificate even if it exists .. versionadded:: 2015.5.1 Writes out a CA certificate based upon defined config values. If the file already exists, the function just returns assuming the CA certificate already exists. If the following values were set:: ca.cert_base_path='/etc/pki' ca_name='koji' the resulting CA, and corresponding key, would be written in the following location with appropriate permissions:: /etc/pki/koji/koji_ca_cert.crt /etc/pki/koji/koji_ca_cert.key CLI Example: .. code-block:: bash salt '*' tls.create_ca test_ca """ status = _check_onlyif_unless(onlyif, unless) if status is not None: return None set_ca_path(cacert_path) if not ca_filename: ca_filename = f"{ca_name}_ca_cert" certp = f"{cert_base_path()}/{ca_name}/{ca_filename}.crt" ca_keyp = f"{cert_base_path()}/{ca_name}/{ca_filename}.key" if not replace and not fixmode and ca_exists(ca_name, ca_filename=ca_filename): return f'Certificate for CA named "{ca_name}" already exists' if fixmode and not os.path.exists(certp): raise ValueError(f"{certp} does not exists, can't fix") if not os.path.exists(f"{cert_base_path()}/{ca_name}"): os.makedirs(f"{cert_base_path()}/{ca_name}") # try to reuse existing ssl key key = None if os.path.exists(ca_keyp): with salt.utils.files.fopen(ca_keyp) as fic2: # try to determine the key bits try: key = OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, fic2.read() ) except OpenSSL.crypto.Error as err: log.warning( "Error loading existing private key %s, generating a new key: %s", ca_keyp, err, ) bck = "{}.unloadable.{}".format( ca_keyp, datetime.utcnow().strftime("%Y%m%d%H%M%S") ) log.info("Saving unloadable CA ssl key in %s", bck) os.rename(ca_keyp, bck) if not key: key = OpenSSL.crypto.PKey() key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) ca = OpenSSL.crypto.X509() ca.set_version(2) ca.set_serial_number(_new_serial(ca_name)) ca.get_subject().C = C ca.get_subject().ST = ST ca.get_subject().L = L ca.get_subject().O = O if OU: ca.get_subject().OU = OU ca.get_subject().CN = CN if emailAddress: ca.get_subject().emailAddress = emailAddress ca.gmtime_adj_notBefore(0) ca.gmtime_adj_notAfter(int(days) * 24 * 60 * 60) ca.set_issuer(ca.get_subject()) ca.set_pubkey(key) if X509_EXT_ENABLED: ca.add_extensions( [ OpenSSL.crypto.X509Extension( b"basicConstraints", True, b"CA:TRUE, pathlen:0" ), OpenSSL.crypto.X509Extension( b"keyUsage", True, b"keyCertSign, cRLSign" ), OpenSSL.crypto.X509Extension( b"subjectKeyIdentifier", False, b"hash", subject=ca ), ] ) ca.add_extensions( [ OpenSSL.crypto.X509Extension( b"authorityKeyIdentifier", False, b"issuer:always,keyid:always", issuer=ca, ) ] ) ca.sign(key, salt.utils.stringutils.to_str(digest)) # always backup existing keys in case keycontent = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) write_key = True if os.path.exists(ca_keyp): bck = "{}.{}".format(ca_keyp, datetime.utcnow().strftime("%Y%m%d%H%M%S")) with salt.utils.files.fopen(ca_keyp) as fic: old_key = salt.utils.stringutils.to_unicode(fic.read()).strip() if old_key.strip() == keycontent.strip(): write_key = False else: log.info("Saving old CA ssl key in %s", bck) fp = os.open(bck, os.O_CREAT | os.O_RDWR, 0o600) with salt.utils.files.fopen(fp, "w") as bckf: bckf.write(old_key) if write_key: fp = os.open(ca_keyp, os.O_CREAT | os.O_RDWR, 0o600) with salt.utils.files.fopen(fp, "wb") as ca_key: ca_key.write(salt.utils.stringutils.to_bytes(keycontent)) with salt.utils.files.fopen(certp, "wb") as ca_crt: ca_crt.write( salt.utils.stringutils.to_bytes( OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, ca) ) ) _write_cert_to_database(ca_name, ca) ret = 'Created Private Key: "{}/{}/{}.key" '.format( cert_base_path(), ca_name, ca_filename ) ret += 'Created CA "{0}": "{1}/{0}/{2}.crt"'.format( ca_name, cert_base_path(), ca_filename ) return ret def get_extensions(cert_type): """ Fetch X509 and CSR extension definitions from tls:extensions: (common|server|client) or set them to standard defaults. .. versionadded:: 2015.8.0 cert_type: The type of certificate such as ``server`` or ``client``. CLI Example: .. code-block:: bash salt '*' tls.get_extensions client """ assert X509_EXT_ENABLED, ( "X509 extensions are not supported in " "pyOpenSSL prior to version 0.15.1. Your " "version: {}".format(OpenSSL_version) ) ext = {} if cert_type == "": log.error( "cert_type set to empty in tls_ca.get_extensions(); " "defaulting to ``server``" ) cert_type = "server" try: ext["common"] = __salt__["pillar.get"]("tls.extensions:common", False) except NameError as err: log.debug(err) if not ext["common"] or ext["common"] == "": ext["common"] = { "csr": {"basicConstraints": "CA:FALSE"}, "cert": { "authorityKeyIdentifier": "keyid,issuer:always", "subjectKeyIdentifier": "hash", }, } try: ext["server"] = __salt__["pillar.get"]("tls.extensions:server", False) except NameError as err: log.debug(err) if not ext["server"] or ext["server"] == "": ext["server"] = { "csr": { "extendedKeyUsage": "serverAuth", "keyUsage": "digitalSignature, keyEncipherment", }, "cert": {}, } try: ext["client"] = __salt__["pillar.get"]("tls.extensions:client", False) except NameError as err: log.debug(err) if not ext["client"] or ext["client"] == "": ext["client"] = { "csr": { "extendedKeyUsage": "clientAuth", "keyUsage": "nonRepudiation, digitalSignature, keyEncipherment", }, "cert": {}, } # possible user-defined profile or a typo if cert_type not in ext: try: ext[cert_type] = __salt__["pillar.get"](f"tls.extensions:{cert_type}") except NameError as e: log.debug( "pillar, tls:extensions:%s not available or " "not operating in a salt context\n%s", cert_type, e, ) retval = ext["common"] for Use in retval: retval[Use].update(ext[cert_type][Use]) return retval def create_csr( ca_name, bits=2048, CN="localhost", C="US", ST="Utah", L="Salt Lake City", O="SaltStack", OU=None, emailAddress=None, subjectAltName=None, cacert_path=None, ca_filename=None, csr_path=None, csr_filename=None, digest="sha256", type_ext=False, cert_type="server", replace=False, ): """ Create a Certificate Signing Request (CSR) for a particular Certificate Authority (CA) ca_name name of the CA bits number of RSA key bits, default is 2048 CN common name in the request, default is "localhost" C country, default is "US" ST state, default is "Utah" L locality, default is "Centerville", the city where SaltStack originated O organization, default is "SaltStack" NOTE: Must the same as CA certificate or an error will be raised OU organizational unit, default is None emailAddress email address for the request, default is None subjectAltName valid subjectAltNames in full form, e.g. to add DNS entry you would call this function with this value: examples: ['DNS:somednsname.com', 'DNS:1.2.3.4', 'IP:1.2.3.4', 'IP:2001:4801:7821:77:be76:4eff:fe11:e51', 'email:me@i.like.pie.com'] .. note:: some libraries do not properly query IP: prefixes, instead looking for the given req. source with a DNS: prefix. To be thorough, you may want to include both DNS: and IP: entries if you are using subjectAltNames for destinations for your TLS connections. e.g.: requests to https://1.2.3.4 will fail from python's requests library w/out the second entry in the above list .. versionadded:: 2015.8.0 cert_type Specify the general certificate type. Can be either `server` or `client`. Indicates the set of common extensions added to the CSR. .. code-block:: cfg server: { 'basicConstraints': 'CA:FALSE', 'extendedKeyUsage': 'serverAuth', 'keyUsage': 'digitalSignature, keyEncipherment' } client: { 'basicConstraints': 'CA:FALSE', 'extendedKeyUsage': 'clientAuth', 'keyUsage': 'nonRepudiation, digitalSignature, keyEncipherment' } type_ext boolean. Whether or not to extend the filename with CN_[cert_type] This can be useful if a server and client certificate are needed for the same CN. Defaults to False to avoid introducing an unexpected file naming pattern The files normally named some_subject_CN.csr and some_subject_CN.key will then be saved replace Replace this signing request even if it exists .. versionadded:: 2015.5.1 Writes out a Certificate Signing Request (CSR) If the file already exists, the function just returns assuming the CSR already exists. If the following values were set:: ca.cert_base_path='/etc/pki' ca_name='koji' CN='test.egavas.org' the resulting CSR, and corresponding key, would be written in the following location with appropriate permissions:: /etc/pki/koji/certs/test.egavas.org.csr /etc/pki/koji/certs/test.egavas.org.key CLI Example: .. code-block:: bash salt '*' tls.create_csr test """ set_ca_path(cacert_path) if not ca_filename: ca_filename = f"{ca_name}_ca_cert" if not ca_exists(ca_name, ca_filename=ca_filename): return 'Certificate for CA named "{}" does not exist, please create it first.'.format( ca_name ) if not csr_path: csr_path = f"{cert_base_path()}/{ca_name}/certs/" if not os.path.exists(csr_path): os.makedirs(csr_path) CN_ext = f"_{cert_type}" if type_ext else "" if not csr_filename: csr_filename = f"{CN}{CN_ext}" csr_f = f"{csr_path}/{csr_filename}.csr" if not replace and os.path.exists(csr_f): return f'Certificate Request "{csr_f}" already exists' key = OpenSSL.crypto.PKey() key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) req = OpenSSL.crypto.X509Req() req.get_subject().C = C req.get_subject().ST = ST req.get_subject().L = L req.get_subject().O = O if OU: req.get_subject().OU = OU req.get_subject().CN = CN if emailAddress: req.get_subject().emailAddress = emailAddress try: extensions = get_extensions(cert_type)["csr"] extension_adds = [] for ext, value in extensions.items(): if isinstance(value, str): value = salt.utils.stringutils.to_bytes(value) extension_adds.append( OpenSSL.crypto.X509Extension( salt.utils.stringutils.to_bytes(ext), False, value ) ) except AssertionError as err: log.error(err) extensions = [] if subjectAltName: if X509_EXT_ENABLED: if isinstance(subjectAltName, str): subjectAltName = [subjectAltName] extension_adds.append( OpenSSL.crypto.X509Extension( b"subjectAltName", False, b", ".join(salt.utils.data.encode(subjectAltName)), ) ) else: raise ValueError( "subjectAltName cannot be set as X509 " "extensions are not supported in pyOpenSSL " "prior to version 0.15.1. Your " "version: {}.".format(OpenSSL_version) ) if X509_EXT_ENABLED: req.add_extensions(extension_adds) req.set_pubkey(key) req.sign(key, salt.utils.stringutils.to_str(digest)) # Write private key and request priv_keyp = f"{csr_path}/{csr_filename}.key" fp = os.open(priv_keyp, os.O_CREAT | os.O_RDWR, 0o600) with salt.utils.files.fopen(fp, "wb+") as priv_key: priv_key.write( salt.utils.stringutils.to_bytes( OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) ) ) with salt.utils.files.fopen(csr_f, "wb+") as csr: csr.write( salt.utils.stringutils.to_bytes( OpenSSL.crypto.dump_certificate_request( OpenSSL.crypto.FILETYPE_PEM, req ) ) ) ret = f'Created Private Key: "{csr_path}{csr_filename}.key" ' ret += f'Created CSR for "{CN}": "{csr_path}{csr_filename}.csr"' return ret def create_self_signed_cert( tls_dir="tls", bits=2048, days=365, CN="localhost", C="US", ST="Utah", L="Salt Lake City", O="SaltStack", OU=None, emailAddress=None, cacert_path=None, cert_filename=None, digest="sha256", replace=False, ): """ Create a Self-Signed Certificate (CERT) tls_dir location appended to the ca.cert_base_path, default is 'tls' bits number of RSA key bits, default is 2048 CN common name in the request, default is "localhost" C country, default is "US" ST state, default is "Utah" L locality, default is "Centerville", the city where SaltStack originated O organization, default is "SaltStack" NOTE: Must the same as CA certificate or an error will be raised OU organizational unit, default is None emailAddress email address for the request, default is None cacert_path absolute path to ca certificates root directory digest The message digest algorithm. Must be a string describing a digest algorithm supported by OpenSSL (by EVP_get_digestbyname, specifically). For example, "md5" or "sha1". Default: 'sha256' replace Replace this certificate even if it exists .. versionadded:: 2015.5.1 Writes out a Self-Signed Certificate (CERT). If the file already exists, the function just returns. If the following values were set:: ca.cert_base_path='/etc/pki' tls_dir='koji' CN='test.egavas.org' the resulting CERT, and corresponding key, would be written in the following location with appropriate permissions:: /etc/pki/koji/certs/test.egavas.org.crt /etc/pki/koji/certs/test.egavas.org.key CLI Example: .. code-block:: bash salt '*' tls.create_self_signed_cert Passing options from the command line: .. code-block:: bash salt 'minion' tls.create_self_signed_cert CN='test.mysite.org' """ set_ca_path(cacert_path) if not os.path.exists(f"{cert_base_path()}/{tls_dir}/certs/"): os.makedirs(f"{cert_base_path()}/{tls_dir}/certs/") if not cert_filename: cert_filename = CN if not replace and os.path.exists( f"{cert_base_path()}/{tls_dir}/certs/{cert_filename}.crt" ): return f'Certificate "{cert_filename}" already exists' key = OpenSSL.crypto.PKey() key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) # create certificate cert = OpenSSL.crypto.X509() cert.set_version(2) cert.gmtime_adj_notBefore(0) cert.gmtime_adj_notAfter(int(days) * 24 * 60 * 60) cert.get_subject().C = C cert.get_subject().ST = ST cert.get_subject().L = L cert.get_subject().O = O if OU: cert.get_subject().OU = OU cert.get_subject().CN = CN if emailAddress: cert.get_subject().emailAddress = emailAddress cert.set_serial_number(_new_serial(tls_dir)) cert.set_issuer(cert.get_subject()) cert.set_pubkey(key) cert.sign(key, salt.utils.stringutils.to_str(digest)) # Write private key and cert priv_key_path = "{}/{}/certs/{}.key".format( cert_base_path(), tls_dir, cert_filename ) fp = os.open(priv_key_path, os.O_CREAT | os.O_RDWR, 0o600) with salt.utils.files.fopen(fp, "wb+") as priv_key: priv_key.write( salt.utils.stringutils.to_bytes( OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) ) ) crt_path = f"{cert_base_path()}/{tls_dir}/certs/{cert_filename}.crt" with salt.utils.files.fopen(crt_path, "wb+") as crt: crt.write( salt.utils.stringutils.to_bytes( OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert) ) ) _write_cert_to_database(tls_dir, cert) ret = 'Created Private Key: "{}/{}/certs/{}.key" '.format( cert_base_path(), tls_dir, cert_filename ) ret += 'Created Certificate: "{}/{}/certs/{}.crt"'.format( cert_base_path(), tls_dir, cert_filename ) return ret def create_ca_signed_cert( ca_name, CN, days=365, cacert_path=None, ca_filename=None, cert_path=None, cert_filename=None, digest="sha256", cert_type=None, type_ext=False, replace=False, ): """ Create a Certificate (CERT) signed by a named Certificate Authority (CA) If the certificate file already exists, the function just returns assuming the CERT already exists. The CN *must* match an existing CSR generated by create_csr. If it does not, this method does nothing. ca_name name of the CA CN common name matching the certificate signing request days number of days certificate is valid, default is 365 (1 year) cacert_path absolute path to ca certificates root directory ca_filename alternative filename for the CA .. versionadded:: 2015.5.3 cert_path full path to the certificates directory cert_filename alternative filename for the certificate, useful when using special characters in the CN. If this option is set it will override the certificate filename output effects of ``cert_type``. ``type_ext`` will be completely overridden. .. versionadded:: 2015.5.3 digest The message digest algorithm. Must be a string describing a digest algorithm supported by OpenSSL (by EVP_get_digestbyname, specifically). For example, "md5" or "sha1". Default: 'sha256' replace Replace this certificate even if it exists .. versionadded:: 2015.5.1 cert_type string. Either 'server' or 'client' (see create_csr() for details). If create_csr(type_ext=True) this function **must** be called with the same cert_type so it can find the CSR file. .. note:: create_csr() defaults to cert_type='server'; therefore, if it was also called with type_ext, cert_type becomes a required argument for create_ca_signed_cert() type_ext bool. If set True, use ``cert_type`` as an extension to the CN when formatting the filename. e.g.: some_subject_CN_server.crt or some_subject_CN_client.crt This facilitates the context where both types are required for the same subject If ``cert_filename`` is `not None`, setting ``type_ext`` has no effect If the following values were set: .. code-block:: text ca.cert_base_path='/etc/pki' ca_name='koji' CN='test.egavas.org' the resulting signed certificate would be written in the following location: .. code-block:: text /etc/pki/koji/certs/test.egavas.org.crt CLI Example: .. code-block:: bash salt '*' tls.create_ca_signed_cert test localhost """ ret = {} set_ca_path(cacert_path) if not ca_filename: ca_filename = f"{ca_name}_ca_cert" if not cert_path: cert_path = f"{cert_base_path()}/{ca_name}/certs" if type_ext: if not cert_type: log.error( "type_ext = True but cert_type is unset. Certificate not written." ) return ret elif cert_type: CN_ext = f"_{cert_type}" else: CN_ext = "" csr_filename = f"{CN}{CN_ext}" if not cert_filename: cert_filename = f"{CN}{CN_ext}" if not replace and os.path.exists( os.path.join( os.path.sep.join( "{}/{}/certs/{}.crt".format( cert_base_path(), ca_name, cert_filename ).split("/") ) ) ): return f'Certificate "{cert_filename}" already exists' try: maybe_fix_ssl_version(ca_name, cacert_path=cacert_path, ca_filename=ca_filename) with salt.utils.files.fopen( f"{cert_base_path()}/{ca_name}/{ca_filename}.crt" ) as fhr: ca_cert = OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, fhr.read() ) with salt.utils.files.fopen( f"{cert_base_path()}/{ca_name}/{ca_filename}.key" ) as fhr: ca_key = OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, fhr.read() ) except OSError: ret["retcode"] = 1 ret["comment"] = f'There is no CA named "{ca_name}"' return ret try: csr_path = f"{cert_path}/{csr_filename}.csr" with salt.utils.files.fopen(csr_path) as fhr: req = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_PEM, fhr.read() ) except OSError: ret["retcode"] = 1 ret["comment"] = 'There is no CSR that matches the CN "{}"'.format( cert_filename ) return ret exts = [] try: exts.extend(req.get_extensions()) except AttributeError: try: # see: http://bazaar.launchpad.net/~exarkun/pyopenssl/master/revision/189 # support is there from quite a long time, but without API # so we mimic the newly get_extensions method present in ultra # recent pyopenssl distros log.info( "req.get_extensions() not supported in pyOpenSSL versions " "prior to 0.15. Processing extensions internally. " "Your version: %s", OpenSSL_version, ) native_exts_obj = OpenSSL._util.lib.X509_REQ_get_extensions(req._req) for i in range(OpenSSL._util.lib.sk_X509_EXTENSION_num(native_exts_obj)): ext = OpenSSL.crypto.X509Extension.__new__(OpenSSL.crypto.X509Extension) ext._extension = OpenSSL._util.lib.sk_X509_EXTENSION_value( native_exts_obj, i ) exts.append(ext) except Exception: # pylint: disable=broad-except log.error( "X509 extensions are unsupported in pyOpenSSL " "versions prior to 0.14. Upgrade required to " "use extensions. Current version: %s", OpenSSL_version, ) cert = OpenSSL.crypto.X509() cert.set_version(2) cert.set_subject(req.get_subject()) cert.gmtime_adj_notBefore(0) cert.gmtime_adj_notAfter(int(days) * 24 * 60 * 60) cert.set_serial_number(_new_serial(ca_name)) cert.set_issuer(ca_cert.get_subject()) cert.set_pubkey(req.get_pubkey()) cert.add_extensions(exts) cert.sign(ca_key, salt.utils.stringutils.to_str(digest)) cert_full_path = f"{cert_path}/{cert_filename}.crt" with salt.utils.files.fopen(cert_full_path, "wb+") as crt: crt.write( salt.utils.stringutils.to_bytes( OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert) ) ) _write_cert_to_database(ca_name, cert) return 'Created Certificate for "{}": "{}/{}.crt"'.format( CN, cert_path, cert_filename ) def create_pkcs12(ca_name, CN, passphrase="", cacert_path=None, replace=False): """ Create a PKCS#12 browser certificate for a particular Certificate (CN) ca_name name of the CA CN common name matching the certificate signing request passphrase used to unlock the PKCS#12 certificate when loaded into the browser cacert_path absolute path to ca certificates root directory replace Replace this certificate even if it exists .. versionadded:: 2015.5.1 If the following values were set:: ca.cert_base_path='/etc/pki' ca_name='koji' CN='test.egavas.org' the resulting signed certificate would be written in the following location:: /etc/pki/koji/certs/test.egavas.org.p12 CLI Example: .. code-block:: bash salt '*' tls.create_pkcs12 test localhost """ set_ca_path(cacert_path) if not replace and os.path.exists(f"{cert_base_path()}/{ca_name}/certs/{CN}.p12"): return f'Certificate "{CN}" already exists' try: with salt.utils.files.fopen( "{0}/{1}/{1}_ca_cert.crt".format(cert_base_path(), ca_name) ) as fhr: ca_cert = OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, fhr.read() ) except OSError: return f'There is no CA named "{ca_name}"' try: with salt.utils.files.fopen( f"{cert_base_path()}/{ca_name}/certs/{CN}.crt" ) as fhr: cert = OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, fhr.read() ) with salt.utils.files.fopen( f"{cert_base_path()}/{ca_name}/certs/{CN}.key" ) as fhr: key = OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, fhr.read() ) except OSError: return f'There is no certificate that matches the CN "{CN}"' pkcs12 = OpenSSL.crypto.PKCS12() pkcs12.set_certificate(cert) pkcs12.set_ca_certificates([ca_cert]) pkcs12.set_privatekey(key) with salt.utils.files.fopen( f"{cert_base_path()}/{ca_name}/certs/{CN}.p12", "wb" ) as ofile: ofile.write( pkcs12.export(passphrase=salt.utils.stringutils.to_bytes(passphrase)) ) return 'Created PKCS#12 Certificate for "{0}": "{1}/{2}/certs/{0}.p12"'.format( CN, cert_base_path(), ca_name, ) def cert_info(cert, digest="sha256"): """ Return information for a particular certificate cert path to the certifiate PEM file or string .. versionchanged:: 2018.3.4 digest what digest to use for fingerprinting CLI Example: .. code-block:: bash salt '*' tls.cert_info /dir/for/certs/cert.pem """ # format that OpenSSL returns dates in date_fmt = "%Y%m%d%H%M%SZ" if "-----BEGIN" not in cert: with salt.utils.files.fopen(cert) as cert_file: cert = cert_file.read() cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert) issuer = {} for key, value in cert.get_issuer().get_components(): if isinstance(key, bytes): key = salt.utils.stringutils.to_unicode(key) if isinstance(value, bytes): value = salt.utils.stringutils.to_unicode(value) issuer[key] = value subject = {} for key, value in cert.get_subject().get_components(): if isinstance(key, bytes): key = salt.utils.stringutils.to_unicode(key) if isinstance(value, bytes): value = salt.utils.stringutils.to_unicode(value) subject[key] = value ret = { "fingerprint": salt.utils.stringutils.to_unicode( cert.digest(salt.utils.stringutils.to_str(digest)) ), "subject": subject, "issuer": issuer, "serial_number": cert.get_serial_number(), "not_before": calendar.timegm( time.strptime( str(cert.get_notBefore().decode(__salt_system_encoding__)), date_fmt ) ), "not_after": calendar.timegm( time.strptime( cert.get_notAfter().decode(__salt_system_encoding__), date_fmt ) ), } # add additional info if your version of pyOpenSSL supports it if hasattr(cert, "get_extension_count"): ret["extensions"] = {} for i in range(cert.get_extension_count()): try: ext = cert.get_extension(i) key = salt.utils.stringutils.to_unicode(ext.get_short_name()) ret["extensions"][key] = str(ext).strip() except AttributeError: continue if "subjectAltName" in ret.get("extensions", {}): valid_entries = ("DNS", "IP Address") valid_names = set() for name in str(ret["extensions"]["subjectAltName"]).split(", "): entry, name = name.split(":", 1) if entry not in valid_entries: log.error( "Cert %s has an entry (%s) which does not start with %s", ret["subject"], name, "/".join(valid_entries), ) else: valid_names.add(name) ret["subject_alt_names"] = list(valid_names) if hasattr(cert, "get_signature_algorithm"): try: value = cert.get_signature_algorithm() if isinstance(value, bytes): value = salt.utils.stringutils.to_unicode(value) ret["signature_algorithm"] = value except AttributeError: # On py3 at least # AttributeError: cdata 'X509 *' points to an opaque type: cannot read fields pass return ret def create_empty_crl( ca_name, cacert_path=None, ca_filename=None, crl_file=None, digest="sha256" ): """ Create an empty Certificate Revocation List. .. versionadded:: 2015.8.0 ca_name name of the CA cacert_path absolute path to ca certificates root directory ca_filename alternative filename for the CA .. versionadded:: 2015.5.3 crl_file full path to the CRL file digest The message digest algorithm. Must be a string describing a digest algorithm supported by OpenSSL (by EVP_get_digestbyname, specifically). For example, "md5" or "sha1". Default: 'sha256' CLI Example: .. code-block:: bash salt '*' tls.create_empty_crl ca_name='koji' \ ca_filename='ca' \ crl_file='/etc/openvpn/team1/crl.pem' """ set_ca_path(cacert_path) if not ca_filename: ca_filename = f"{ca_name}_ca_cert" if not crl_file: crl_file = f"{_cert_base_path()}/{ca_name}/crl.pem" if os.path.exists(f"{crl_file}"): return f'CRL "{crl_file}" already exists' try: with salt.utils.files.fopen( f"{cert_base_path()}/{ca_name}/{ca_filename}.crt" ) as fp_: ca_cert = OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, fp_.read() ) with salt.utils.files.fopen( f"{cert_base_path()}/{ca_name}/{ca_filename}.key" ) as fp_: ca_key = OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, fp_.read() ) except OSError: return f'There is no CA named "{ca_name}"' crl = OpenSSL.crypto.CRL() crl_text = crl.export( ca_cert, ca_key, digest=salt.utils.stringutils.to_bytes(digest), ) with salt.utils.files.fopen(crl_file, "w") as f: f.write(salt.utils.stringutils.to_str(crl_text)) return f'Created an empty CRL: "{crl_file}"' def revoke_cert( ca_name, CN, cacert_path=None, ca_filename=None, cert_path=None, cert_filename=None, crl_file=None, digest="sha256", ): """ Revoke a certificate. .. versionadded:: 2015.8.0 ca_name Name of the CA. CN Common name matching the certificate signing request. cacert_path Absolute path to ca certificates root directory. ca_filename Alternative filename for the CA. cert_path Path to the cert file. cert_filename Alternative filename for the certificate, useful when using special characters in the CN. crl_file Full path to the CRL file. digest The message digest algorithm. Must be a string describing a digest algorithm supported by OpenSSL (by EVP_get_digestbyname, specifically). For example, "md5" or "sha1". Default: 'sha256' CLI Example: .. code-block:: bash salt '*' tls.revoke_cert ca_name='koji' \ ca_filename='ca' \ crl_file='/etc/openvpn/team1/crl.pem' """ set_ca_path(cacert_path) ca_dir = f"{cert_base_path()}/{ca_name}" if ca_filename is None: ca_filename = f"{ca_name}_ca_cert" if cert_path is None: cert_path = f"{_cert_base_path()}/{ca_name}/certs" if cert_filename is None: cert_filename = f"{CN}" try: with salt.utils.files.fopen( f"{cert_base_path()}/{ca_name}/{ca_filename}.crt" ) as fp_: ca_cert = OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, fp_.read() ) with salt.utils.files.fopen( f"{cert_base_path()}/{ca_name}/{ca_filename}.key" ) as fp_: ca_key = OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, fp_.read() ) except OSError: return f'There is no CA named "{ca_name}"' client_cert = _read_cert(f"{cert_path}/{cert_filename}.crt") if client_cert is None: return f'There is no client certificate named "{CN}"' index_file, expire_date, serial_number, subject = _get_basic_info( ca_name, client_cert, ca_dir ) index_serial_subject = f"{serial_number}\tunknown\t{subject}" index_v_data = f"V\t{expire_date}\t\t{index_serial_subject}" index_r_data_pattern = re.compile( r"R\t" + expire_date + r"\t\d{12}Z\t" + re.escape(index_serial_subject) ) index_r_data = "R\t{}\t{}\t{}".format( expire_date, _four_digit_year_to_two_digit(datetime.utcnow()), index_serial_subject, ) ret = {} with salt.utils.files.fopen(index_file) as fp_: for line in fp_: line = salt.utils.stringutils.to_unicode(line) if index_r_data_pattern.match(line): revoke_date = line.split("\t")[2] try: datetime.strptime(revoke_date, two_digit_year_fmt) return '"{}/{}.crt" was already revoked, serial number: {}'.format( cert_path, cert_filename, serial_number ) except ValueError: ret["retcode"] = 1 ret["comment"] = ( "Revocation date '{}' does not matchformat '{}'".format( revoke_date, two_digit_year_fmt ) ) return ret elif index_serial_subject in line: __salt__["file.replace"]( index_file, index_v_data, index_r_data, backup=False ) break crl = OpenSSL.crypto.CRL() with salt.utils.files.fopen(index_file) as fp_: for line in fp_: line = salt.utils.stringutils.to_unicode(line) if line.startswith("R"): fields = line.split("\t") revoked = OpenSSL.crypto.Revoked() revoked.set_serial(salt.utils.stringutils.to_bytes(fields[3])) revoke_date_2_digit = datetime.strptime(fields[2], two_digit_year_fmt) revoked.set_rev_date( salt.utils.stringutils.to_bytes( revoke_date_2_digit.strftime(four_digit_year_fmt) ) ) crl.add_revoked(revoked) crl_text = crl.export( ca_cert, ca_key, digest=salt.utils.stringutils.to_bytes(digest) ) if crl_file is None: crl_file = f"{_cert_base_path()}/{ca_name}/crl.pem" if os.path.isdir(crl_file): ret["retcode"] = 1 ret["comment"] = f'crl_file "{crl_file}" is an existing directory' return ret with salt.utils.files.fopen(crl_file, "w") as fp_: fp_.write(salt.utils.stringutils.to_str(crl_text)) return 'Revoked Certificate: "{}/{}.crt", serial number: {}'.format( cert_path, cert_filename, serial_number ) if __name__ == "__main__": # create_ca('koji', days=365, **cert_sample_meta) create_csr( "koji", CN="test_system", C="US", ST="Utah", L="Centerville", O="SaltStack", OU=None, emailAddress="test_system@saltproject.io", ) create_ca_signed_cert("koji", "test_system") create_pkcs12("koji", "test_system", passphrase="test")