From 4bb49d159f7ecb46950ef4943f9519df1dd1361c Mon Sep 17 00:00:00 2001 From: Colin Sullivan Date: Wed, 21 Feb 2024 13:26:51 -0700 Subject: [PATCH 1/3] add ca_certs_search for windows cert store Signed-off-by: Colin Sullivan --- server/certstore/certstore_other.go | 4 +- server/certstore/certstore_windows.go | 80 +++++++++++++++++- server/certstore/errors.go | 3 + server/certstore_windows_test.go | 49 ++++++++--- server/opts.go | 25 +++++- test/configs/certs/tlsauth/certstore/ca.p12 | Bin 0 -> 1067 bytes .../certs/tlsauth/certstore/client.p12 | Bin 2509 -> 2579 bytes .../certstore/delete-cert-from-store.ps1 | 7 +- .../certs/tlsauth/certstore/import-p12-ca.ps1 | 7 ++ .../configs/certs/tlsauth/certstore/pkcs12.md | 4 + .../certs/tlsauth/certstore/server.p12 | Bin 2533 -> 2595 bytes .../certs/tlsauth/certstore/update_keys.bat | 4 + 12 files changed, 163 insertions(+), 20 deletions(-) create mode 100644 test/configs/certs/tlsauth/certstore/ca.p12 create mode 100644 test/configs/certs/tlsauth/certstore/import-p12-ca.ps1 create mode 100644 test/configs/certs/tlsauth/certstore/update_keys.bat diff --git a/server/certstore/certstore_other.go b/server/certstore/certstore_other.go index a72df834a1a..e6e8aee73d4 100644 --- a/server/certstore/certstore_other.go +++ b/server/certstore/certstore_other.go @@ -26,8 +26,8 @@ var _ = MATCHBYEMPTY // otherKey implements crypto.Signer and crypto.Decrypter to satisfy linter on platforms that don't implement certstore type otherKey struct{} -func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, config *tls.Config) error { - _, _, _, _ = certStore, certMatchBy, certMatch, config +func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, caCertsMatch []string, config *tls.Config) error { + _, _, _, _, _ = certStore, certMatchBy, certMatch, caCertsMatch, config return ErrOSNotCompatCertStore } diff --git a/server/certstore/certstore_windows.go b/server/certstore/certstore_windows.go index 57adc187abf..1093c918541 100644 --- a/server/certstore/certstore_windows.go +++ b/server/certstore/certstore_windows.go @@ -112,7 +112,10 @@ var ( } // MY is well-known system store on Windows that holds personal certificates - winMyStore = winWide("MY") + winMyStore = winWide("MY") + winIntermediateCAStore = winWide("CA") + winRootStore = winWide("Root") + winAuthRootStore = winWide("AuthRoot") // These DLLs must be available on all Windows hosts winCrypt32 = windows.MustLoadDLL("crypt32.dll") @@ -137,9 +140,38 @@ type winPSSPaddingInfo struct { cbSalt uint32 } +// createCACertsPool generates a CertPool from the Windows certificate store, +// adding all matching certificates from the caCertsMatch array to the pool. +// All matching certificates (vs first) are added to the pool based on a user +// request. If no certificates are found an error is returned. +func createCACertsPool(cs *winCertStore, storeType uint32, caCertsMatch []string) (*x509.CertPool, error) { + var errs []error + var count int + caPool := x509.NewCertPool() + for _, s := range caCertsMatch { + lfs, err := cs.caCertsBySubjectMatch(s, storeType) + if err != nil { + if errs == nil { + errs = make([]error, 0) + } + errs = append(errs, err) + } else { + for _, lf := range lfs { + caPool.AddCert(lf) + count++ + } + } + } + // If no certs were added, return the errors. + if count == 0 { + return nil, fmt.Errorf("unable to match any CA certificate: %v", errs) + } + return caPool, nil +} + // TLSConfig fulfills the same function as reading cert and key pair from pem files but // sources the Windows certificate store instead -func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, config *tls.Config) error { +func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, caCertsMatch []string, config *tls.Config) error { var ( leaf *x509.Certificate leafCtx *windows.CertContext @@ -186,6 +218,14 @@ func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, c if pk == nil { return ErrNoPrivateKeyStoreRef } + // Look for CA Certificates + if caCertsMatch != nil { + caPool, err := createCACertsPool(cs, scope, caCertsMatch) + if err != nil { + return err + } + config.ClientCAs = caPool + } } else { return ErrBadCertStore } @@ -319,6 +359,42 @@ func (w *winCertStore) certBySubject(subject string, storeType uint32) (*x509.Ce return w.certSearch(winFindSubjectStr, subject, winMyStore, storeType) } +// caCertBySubject matches and returns the all matching certificates of the subject field. +// +// The following locations are searched: +// 1) Root (Trusted Root Certification Authorities) +// 2) AuthRoot (Third-Party Root Certification Authorities) +// 3) CA (Intermediate Certification Authorities) +// +// Caller specifies current user's personal certs or local machine's personal certs using storeType. +// See CERT_FIND_SUBJECT_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore +func (w *winCertStore) caCertsBySubjectMatch(subject string, storeType uint32) ([]*x509.Certificate, error) { + var ( + leaf *x509.Certificate + searchLocations = [3]*uint16{winRootStore, winAuthRootStore, winIntermediateCAStore} + ) + // surprisingly, an empty string returns a result. We'll treat this as an error. + if subject == "" { + return nil, ErrFailedCertSearch + } + rv := make([]*x509.Certificate, 0) + for _, sr := range searchLocations { + var err error + if leaf, _, err = w.certSearch(winFindSubjectStr, subject, sr, storeType); err == nil { + rv = append(rv, leaf) + } else { + if err != ErrFailedCertSearch { + return nil, err + } + } + } + // Not found anywhere + if len(rv) == 0 { + return nil, ErrFailedCertSearch + } + return rv, nil +} + // certSearch is a helper function to lookup certificates based on search type and match value. // store is used to specify which store to perform the lookup in (system or user). func (w *winCertStore) certSearch(searchType uint32, matchValue string, searchRoot *uint16, store uint32) (*x509.Certificate, *windows.CertContext, error) { diff --git a/server/certstore/errors.go b/server/certstore/errors.go index bbb1c9d83ed..be8b888c058 100644 --- a/server/certstore/errors.go +++ b/server/certstore/errors.go @@ -68,6 +68,9 @@ var ( // ErrBadCertMatchField represents malformed cert_match option ErrBadCertMatchField = errors.New("expected 'cert_match' to be a valid non-empty string") + // ErrBadCaCertMatchField represents malformed cert_match option + ErrBadCaCertMatchField = errors.New("expected 'ca_certs_match' to be a valid non-empty string array") + // ErrOSNotCompatCertStore represents cert_store passed that exists but is not valid on current OS ErrOSNotCompatCertStore = errors.New("cert_store not compatible with current operating system") ) diff --git a/server/certstore_windows_test.go b/server/certstore_windows_test.go index e0f33c2e746..2cb8bc69ef5 100644 --- a/server/certstore_windows_test.go +++ b/server/certstore_windows_test.go @@ -41,7 +41,7 @@ func runPowershellScript(scriptFile string, args []string) error { return cmdImport.Run() } -func runConfiguredLeaf(t *testing.T, hubPort int, certStore string, matchBy string, match string, expectedLeafCount int) { +func runConfiguredLeaf(t *testing.T, hubPort int, certStore string, matchBy string, match string, caMatch string, expectedLeafCount int) { // Fire up the leaf u, err := url.Parse(fmt.Sprintf("nats://localhost:%d", hubPort)) @@ -59,18 +59,18 @@ func runConfiguredLeaf(t *testing.T, hubPort int, certStore string, matchBy stri cert_store: "%s" cert_match_by: "%s" cert_match: "%s" + ca_certs_match: %s # Above should be equivalent to: # cert_file: "../test/configs/certs/tlsauth/client.pem" # key_file: "../test/configs/certs/tlsauth/client-key.pem" - - ca_file: "../test/configs/certs/tlsauth/ca.pem" + # ca_file: "../test/configs/certs/tlsauth/ca.pem" timeout: 5 } } ] } - `, u.String(), certStore, matchBy, match) + `, u.String(), certStore, matchBy, match, caMatch) leafConfig := createConfFile(t, []byte(configStr)) defer removeFile(t, leafConfig) @@ -90,7 +90,7 @@ func runConfiguredLeaf(t *testing.T, hubPort int, certStore string, matchBy stri func TestLeafTLSWindowsCertStore(t *testing.T) { // Client Identity (client.pem) - // Issuer: O = Synadia Communications Inc., OU = NATS.io, CN = localhost + // Issuer: O = NATS CA, OU = NATS.io, CN = localhost // Subject: OU = NATS.io, CN = example.com // Make sure windows cert store is reset to avoid conflict with other tests @@ -105,6 +105,11 @@ func TestLeafTLSWindowsCertStore(t *testing.T) { t.Fatalf("expected powershell provision to succeed: %s", err.Error()) } + err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-ca.ps1", nil) + if err != nil { + t.Fatalf("expected powershell provision CA to succeed: %s", err.Error()) + } + // Fire up the hub hubConfig := createConfFile(t, []byte(` port: -1 @@ -140,26 +145,38 @@ func TestLeafTLSWindowsCertStore(t *testing.T) { certStore string certMatchBy string certMatch string + caCertsMatch string expectedLeafCount int }{ - {"WindowsCurrentUser", "Subject", "example.com", 1}, - {"WindowsCurrentUser", "Issuer", "Synadia Communications Inc.", 1}, - {"WindowsCurrentUser", "Issuer", "Frodo Baggins, Inc.", 0}, + // Test subject and issuer + {"WindowsCurrentUser", "Subject", "example.com", "\"NATS CA\"", 1}, + {"WindowsCurrentUser", "Issuer", "NATS CA", "\"NATS CA\"", 1}, + {"WindowsCurrentUser", "Issuer", "Frodo Baggins, Inc.", "\"NATS CA\"", 0}, + // Test CAs, NATS CA is valid, others are missing + {"WindowsCurrentUser", "Subject", "example.com", "[\"NATS CA\"]", 1}, + {"WindowsCurrentUser", "Subject", "example.com", "[\"GlobalSign\"]", 0}, + {"WindowsCurrentUser", "Subject", "example.com", "[\"Missing NATS Cert\"]", 0}, + {"WindowsCurrentUser", "Subject", "example.com", "[\"NATS CA\", \"Missing NATS Cert1\"]", 1}, + {"WindowsCurrentUser", "Subject", "example.com", "[\"Missing Cert2\",\"NATS CA\"]", 1}, + {"WindowsCurrentUser", "Subject", "example.com", "[\"Missing, Cert3\",\"Missing NATS Cert4\"]", 0}, } for _, tc := range testCases { - t.Run(fmt.Sprintf("%s by %s match %s", tc.certStore, tc.certMatchBy, tc.certMatch), func(t *testing.T) { + testName := fmt.Sprintf("%s by %s match %s", tc.certStore, tc.certMatchBy, tc.certMatch) + t.Run(fmt.Sprintf(testName, tc.certStore, tc.certMatchBy, tc.certMatch, tc.caCertsMatch), func(t *testing.T) { defer func() { if r := recover(); r != nil { if tc.expectedLeafCount != 0 { - t.Fatalf("did not expect panic") + t.Fatalf("did not expect panic: %s", testName) } else { if !strings.Contains(fmt.Sprintf("%v", r), "Error processing configuration file") { - t.Fatalf("did not expect unknown panic cause") + t.Fatalf("did not expect unknown panic: %s", testName) } } } }() - runConfiguredLeaf(t, hubOptions.LeafNode.Port, tc.certStore, tc.certMatchBy, tc.certMatch, tc.expectedLeafCount) + runConfiguredLeaf(t, hubOptions.LeafNode.Port, + tc.certStore, tc.certMatchBy, tc.certMatch, + tc.caCertsMatch, tc.expectedLeafCount) }) } } @@ -169,7 +186,7 @@ func TestLeafTLSWindowsCertStore(t *testing.T) { func TestServerTLSWindowsCertStore(t *testing.T) { // Server Identity (server.pem) - // Issuer: O = Synadia Communications Inc., OU = NATS.io, CN = localhost + // Issuer: O = NATS CA, OU = NATS.io, CN = localhost // Subject: OU = NATS.io Operators, CN = localhost // Make sure windows cert store is reset to avoid conflict with other tests @@ -184,6 +201,11 @@ func TestServerTLSWindowsCertStore(t *testing.T) { t.Fatalf("expected powershell provision to succeed: %s", err.Error()) } + err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-ca.ps1", nil) + if err != nil { + t.Fatalf("expected powershell provision CA to succeed: %s", err.Error()) + } + // Fire up the server srvConfig := createConfFile(t, []byte(` listen: "localhost:-1" @@ -191,6 +213,7 @@ func TestServerTLSWindowsCertStore(t *testing.T) { cert_store: "WindowsCurrentUser" cert_match_by: "Subject" cert_match: "NATS.io Operators" + ca_certs_match: ["NATS CA"] timeout: 5 } `)) diff --git a/server/opts.go b/server/opts.go index c9b147e9108..0df33eb0319 100644 --- a/server/opts.go +++ b/server/opts.go @@ -673,6 +673,7 @@ type TLSConfigOpts struct { CertStore certstore.StoreType CertMatchBy certstore.MatchByType CertMatch string + CaCertsMatch []string OCSPPeerConfig *certidp.OCSPPeerConfig Certificates []*TLSCertPairOpt } @@ -4441,6 +4442,28 @@ func parseTLS(v interface{}, isClientCtx bool) (t *TLSConfigOpts, retErr error) return nil, &configErr{tk, certstore.ErrBadCertMatchField.Error()} } tc.CertMatch = certMatch + case "ca_certs_match": + rv := []string{} + switch mv := mv.(type) { + case string: + rv = append(rv, mv) + case []string: + rv = append(rv, mv...) + case []interface{}: + for _, t := range mv { + if token, ok := t.(token); ok { + if ts, ok := token.Value().(string); ok { + rv = append(rv, ts) + continue + } else { + return nil, &configErr{tk, fmt.Sprintf("error parsing ca_cert_match: unsupported type %T where string is expected", token)} + } + } else { + return nil, &configErr{tk, fmt.Sprintf("error parsing ca_cert_match: unsupported type %T", t)} + } + } + } + tc.CaCertsMatch = rv case "handshake_first", "first", "immediate": switch mv := mv.(type) { case bool: @@ -4835,7 +4858,7 @@ func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) { } config.Certificates = []tls.Certificate{cert} case tc.CertStore != certstore.STOREEMPTY: - err := certstore.TLSConfig(tc.CertStore, tc.CertMatchBy, tc.CertMatch, &config) + err := certstore.TLSConfig(tc.CertStore, tc.CertMatchBy, tc.CertMatch, tc.CaCertsMatch, &config) if err != nil { return nil, err } diff --git a/test/configs/certs/tlsauth/certstore/ca.p12 b/test/configs/certs/tlsauth/certstore/ca.p12 new file mode 100644 index 0000000000000000000000000000000000000000..d9b67effe0f379f19f89e482b7ce4d3314dbe9ac GIT binary patch literal 1067 zcmXqLVo_&eWHxAGzRSj`)#lOmotKfFaX}OFIhH2oQ$XQkKzs-x#Rinx3=~?=#K-`o zRv=^;4Z`6ZR$c=y15E^%$3TXKBkknXLYpU|$C;Qo7z}vWI3Tt$ak4TP$g*)Jw0SV5 zGP5vhu?V!y*KGdDe)wKlefyqVp6y#2nwTBz@BQjH+{@7o$cQm<^^kC znb&eK|GaTwdd%X7lFOD_|JHxak=Q@^Zrt~)_jj{sMf4pOdyz5e-r5h-c)MKk@{fF( zzisP6pBzqYKX>VkvHxV*rlwuezV~#$XV>Ya6VxIMcqKjGPGS$9DhgY;0$~ zO{qBJ+99`rD^PRoiMiz=v#S)Djvl+v!THKUsX`>|zhT^Wp;LEU-}GPO6q8nQ*z>Mj ze66pQP#B{*qtjCr_23uA>CgBosyCbSd}Dp2n&fl9>*DH#>5l`xDKc*1ijkPV@Zzud z`n$7LyO>_DJ+$s&p24ra=^3unn&f^>7k`AcZ-=kK)q7%h9wpldae`4`MleKJlnsWQ)3l~eiA3fvz zXYupcJVoDy-h7H>4nI|oWi!jJ`&K4$(edrxGi#dzQoeqX=W*V7Q*};<+Wh}%XMCDx zw*)@Dd9f~fo@(mVV`^Gv-GSi=`BN7jyOnTA@L568PsOe?CwE`sz36mv$Ju$ee_QUG zWG2P-<09k2Wp$6mp0PK{tbgA#D{X)G*G=zU>=pjdpzq5$A>(aw7~ik|+j$Gzt5~tIZ>Y2Nl?%vFNXkg6jE4t;@f?0BtrzIN|^~7jtF!1s9 z%=;@kcOUCk$<+$E8e8P%_=pvEN$#;b^tN}RS?vTindKii%iI}m?!f{pQ7?Z}zZyumL3jlD3)U*Hq literal 0 HcmV?d00001 diff --git a/test/configs/certs/tlsauth/certstore/client.p12 b/test/configs/certs/tlsauth/certstore/client.p12 index 18ee5c32f01bd2c89fdff8779f6b309242426da8..3c2e93a93ac4555873652e89aa5ab0325a08bb1e 100644 GIT binary patch literal 2579 zcmai$X*3iJ7stn#VMexzsEi&<3MIp!VWw2ZzB5DiC2Ng+8xk{PH{n6qmn>N$`;x7Y ztQoSCD2ga!ONH@z&ikI8kMDt z2?g2!qCjho?Meg%B=T>>@`M87Ikt|60LKmTZ^DEy{Ug8}2rY!fzmgpR2SHc``0xqV zI06s|0U+2}AphM4gn|JGK^CZOqz02CGl)qN#9g+ZyKb7{yo-mU-y4lr8IdT=PhMhG zxG1%zI%h^q^`|fpGt0TE;-NRL6Aq70ASC)>nzp44QF+j=!@~UyuoT=IHtVSJI6D9I z%rfh`f0nSHTS=`3^;Mg|cx^x{;C5PIubvd!Dk#+A+DlhvB!Fo$c=(PUzdj6pLn7oT;KU%(k zB&lb2r~??hh|O}JQg&LaJRtW=D$fk!#!kK3_NiR^z96(1*`Bg2 zYe>!8ddIvK*h`S=Gc?f+AChY%dVW!rzc#jWr!3cn>E;x&*ziEeXoa~0GP>AUqd>GQs-W$PETGky+B&CXK`x@Po>(Er>AjiMfn&MD;4FEsm$(WdZpu z+NrYlw}0dwH*dq(61T?9-$Wy!imB$&yz-h)yea3?UUhvmk$0yG(p5GT&y)v6qY1In zAqOQ}bDh^Fwj5dS|VJt$RKIf`Ogu; z%peMg78WOfS+De(kY+KF%x7`TnWL;^$LzW`aLG<*RJp0fghVCgdruqqBnnw|O{ayT zAC0z=zI^sMPjB0_vEMR|+UsON-oHLlt~_Uni9)Izfry`%4b=?1Z35wma*6-N9<553EugggVa!%B?1-<{Eg(O^oa~5OcrW}c4;0n0l<-5NxZZ!~K0@HsBelM4#J`c+MM zZo?kH8^7lmO^VTZaGa4#ukSpkGz?;EFMv8p!i9Fmb$Eg^PF3qe%5sUSGEUOs({Qc9 zQ42kN50xk^`@%xvFZOKdm(sp}m&9zfGArG=@@+q?LO4_LonKFgp{Nu#{zh))C%sp2 z<<$#ou{nmxzNcu`0rfoBYAjr@@aqP@F6Y)J(`q3D;hq3kV~a`Ipw`zb>AplEw4d=V zjEs|x;6y21CE1_3NyC#JvX0F4D_8}7?N)1WKK+|}C*kgfX@&l?tJK0d3_Tik)uz9d zUZ4Ip?od(h2j`kOl$Wir|!fJd_|g-y99Uouc7^($|t?O z?T{izT|y^86_rM0oq3u%Q70OS)pX}!W2?MBrOO7-=6{*37C1u{AjwC7UNoo;C;9uO zA+GcI7RDEZp1|l;_o!PL@sfiO(hD8@LpF`!Azqby6-CwY!*|PJKVQ9H?YrD!hV{Og zMZ+GtI%9{Z+AUANUecNMQG;8fFhAZ-_h{#DLJPnx=1B|FQnN?&wasnrCX`*r8TNo%Q^3LbAV%35~djd42tsmNB%*>4jgZm#rE<7_S+a_G`PED}Hz&-f zNzxZ7blF>=>_~4?5U+!03xMyLVHzT0fcOpmqdepxL$3{y<_GLzHZBMhFBB+hFukw! z>{pHDW+&#hXo(IicijD+O$uK5axQq%+Q}4Ih2JgwGGn0Gnp;NVWly||d*u54Zs;Md zjWaxzOHrc5>jiX>WeqNPG4QH&$PoOYS*)R=uWntal5uIDy^I(_^bcU6r)2m*ydl^f zB24RwL^Bo8UR~?*AnxT78F3RKgW&k{P63&~0FY4h*lVA?O@ha;9f>7FOdI3Y4;Lb^ bSA&a^7tBV^LPbH4b%v-{!t;;Be^C1$acQef literal 2509 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 U+E{c!tg Note: set the PKCS12 bundle password to `s3cr3t` as required by provisioning scripts ## Cert Store Provisioning Scripts diff --git a/test/configs/certs/tlsauth/certstore/server.p12 b/test/configs/certs/tlsauth/certstore/server.p12 index 9325afbc15cd755c85b08a3307aa55d4b6547e85..f7a1c4d013540542555d4e2ebdacbec978b734e8 100644 GIT binary patch literal 2595 zcmai$X*3jy8^+BvhOs5aelbjztTWQckbP^eHKfRrAxT+EWR1DDNNQwXhp~k*vhOBk zY?m~VB_c(326t?6-ShvS?#KJ#J@0v*=e+0r^#0C^z_Ez~0jCf+$OsrB7h@8`-~_M$ z@^O%E5DxO@#BN96Kq~)6;Cvhieqx;rF`YEQe-i-0?w4TZL|_o-{*@ev-$1Mxn1snV zi9!Mp$jXG^0JHvY8xX?Ggb)QoTw+WB?x#QiIS_X!B^`Zwror>o^6K~B#qIbaaHq)1 zxUI+O_kD=@76Uk`S!%GvS&XEsH(&D>|5G&*$ielp-dLo&4f|N20vi>_i-`=DC{c+bbwOmo~-z z_Rz@x4DI1zQKG*6eILw>*dR(X3k>=b-9TG4-+Rz>SX}Z=?wpdBGwQ>4|Ky=$`gBUY z*(^?@1KNJOAoizQDMGqYQ}rGCX_htfPxopt2k%)wPbYB2_ZojhrhA*IyjzeF%zHL= zZ3?O~{xqF=1D@skLglSY%YlSYk(wJ@kte0|Mi<~^Z@Hox#Z!h6(%c#G+~%1u{m^e) zwUz(Iq`9~T$5#CUZFXGi$&<=Dg4B31;R5G!YF7YKV*kS)*1r|4#xpPvP=AE4JzkAt zwRaxluQg(GO|&)Sg3o$r|1Jazefcx&Jp%GK-UB;m}f5V({-|CDB7E23juknl1x00yNN9xdJS#NHvskWm;2Aq;J6$R5-NsE63^6C^T-$tnm!mlzZyBEJ|r7 zxbCLpo8I*sg@ezU9}0(TJ37hfjta*|yN<`BOCWQOFSVL`h6-F^%!|=jKv!mcfvQ{e zhxWs21@Ax3RVg24zi14^L+*VOA75jgeiIcxkaiPNWV19Je#{%1rgQ#5&d-m5XO=$b z-y#{nx`sV>4YSh>$gb?=Wc~dJ?R(fnxW4-v&bdJC{C3Hs_JlHPm1AR{9%`RMc$74&yD}!*L_bg9h_*>>wrDOxC1NZdaCU?lXD9aG{akSA=`DyL! zKRHLj!$f!Ky%By&jC2^^O)~7xh^2Ix90u$KbtJ&9m4sp9sR0JS`tNqpBJSie_Oo=g z`R9jybOVXbEq$-SJ)_)@SfNMx!==M?Lc|P_VI+hVr?{ZnahhQ3B>6D1H-dyYsak(GLukEyv zY;?_>*ie>tW@KW2Bu;F-yhWG)GfZTdOFKVk@5(6FB#T9weNtU{TDfR+|Kn}nhgVTb ze$ByJPu}q{4f;(d+AkV?@%)Dc0;IQ+*b|G393N%0xy+l}72?$|f0X zE+dl!ni~hYaLGQeSM$Q3iJ@?J7Xd;VwnLU{mAy+eE`f4*pw4vsf<0Rk=ze!GH4%{6sj$W}v@V?SuEkEwdD7QT=4ew* zcRlHG1!fJW6Ydaycl<9#_VV$7XjB(7V2nkE=5;kTBGT@;kRg^tPU9q@)030(Lg44` z^|yHlt69!-51Zz-&UIudT)aWnC86esc%ib=9+8&MoA?5-K6Pl4#+|!Q@(=YqxRZ#K zLco6k7Q%<*2SLA#5j0;UUABp^$BDQF7Dp#s)_si7MIaHJzuqe#fSCyd)2Z6dbzO=u p^e*1J)e2xR?L{ literal 2533 zcmY+^cQhM{9tUt4O~tOQm#QdktVV2V?@>D~MQyQXt(#b(N^B9*w5lkshFZ01_8Kj! zR$8N_#jV=ZikeT)dGFo#{`j5WIp6O&-#%w7Yr02Rps2f86S@Y zf2$ms_-c003}Dje-#TThI1+bj)|@qQlr95E$dPa7Kt!&GqS#^Hk9RsN#JT15=>Quw zC`3f_NfbBL*stak({qc9@EUi#n>QI|=<##K`uFJ>$q8eZ_`k?}UJpD5KxIBCmIyU3 zZk42$gnYxnx;x3J6LQ038lIk{A5$%@^3@`+qqpYPdYk<0?y30kPxHdK4Om|~q;2m|viM4)h6saVGK{?94@DLN~#gCR#(I|Tn%Xj+> zD_{Jn+)9OIV|oa?0x@vNM7z1r3b%5}6E&g0MBd`QR`exnz58jeoJ!>iSLoYtc&g|u zo1d#-$Zy|1CqtA5L$1k5A3o|u&JW9yW{rrGBvYH^7Il1j)QA1?N|RB5c0)+I$dF{F z&)nBy3I}huZ@iEDqpI!I)Q2X|a$>~<_ae!j{32!?BkuP#8W)AX#{T6x-sHO(?z}63 zjvC|Cg+S$Vt^=;dwh)7|p9t+#YkYbTU@gc58|v-`PGLn|pLqnD#c zWdD^j^VzRU3{cVBZ7nbs8Ks{`Ooi#@)C4-PzRj9xEs}`E8k)`a-5)1SlZFHWa!bu} zs{0EPXv*{}yB4lkuOOXQ*9X{*^R}%X!nG6!KRnC3 zt;H1@quPWt+B!h?h zp^?p$)^f>lUz2hLjI*(>hc7Amn$vXQ~B9x*F-Da z8rImgRU4akzWU@~FU2Ph95^}|)EQyMsL?+faoH`*<+ST$F;JsuheftpZJhU;YsN=O zl>X}Ph3xRo;q_P}oNb*#7=XjgV@(?`%k`+NG-PyxQJ#Mfrj7Zc!_*;haLTm{%Z~^q zt@=;esn?(0d{ky-9D*LeKh|Q8zxOfwYJH)~X2Bgc8#Zo=a}l>Z!;kK#d&6Y%Xo#Yp zctBYXYnDt^Om|HtRi&z(>_6yz5uD#`1eTU-*!^%NxL#G#R;N+TnI`SY>um)&GZplo z=&%4v)>rE|GWq1f$84)|{fHRCfnhQzNa7V1zZOzVM8{2E29zLh7qtEdjHkE@NHFe# z^m#0PexeY@|M0;80G>OuaSqJN|EU7{rOLZ~YDp$=2K-AEFb@2d!Lkc?{G3A(jZ7ha zxzvAHOQ7CXLRkV7`e$3DjPC2|Ug7{YXdDL3a~mq+jTp*ivEE&Y6LK)awneU^HJA;` zu(!k$4v8V^<#eiS)vwG`fyXzcrIOm!lIj_i7KUp*y6X3;WyeZu}Yb1!2 z52t@)88vb3i(+ZbguLiKO(Nr{I<3F0)R^7JJl+Wz&;$pTf%x^H7eUefx(?7YX# zgu3>(Fn~<$jk3!w1z$17PFMH>9`U)Omue`-t*M6$+5w(*UQ_#f7KewK!RF5xMF;20 zpH1n-?7$N(i+|dRKtcjnk$A-?-Mu>azeDY(Z#6qz}BUHfPdH@O+LDB-bN1CCg82HG4M0 zqx^fG$4$~TtuE$ufAK-P6YeL>2EKoTk$z z{@#rS>%_7nJ`s$gQI?fQ%XZjoH)l`COr0_S&*9SN=3CB8qZVI{81@G_I~Vuun5VbW z^uCksRrp-LRqk7NrE^(%v-H3o##;LN{z%h!i*9Cf=(Oyi;!xC|9D7}1D5Y^&{N_<4 zl-aKr`p!6~!00!E^#?`^4JYgevEE95wr#Oea-tsktc@j;;2w+?-q6{CX|K{=Ly-7bXIbK&@4a-!tsegVNtkz;Y#p)oHDp5aXfKhEsJqYS!_$Cb$ zd|4viz@NB>ZOwKb*cSX$?8VQq3+QVeRw1k9nP~+Or>pxrLPDDgSEYOswD$w3ApPtRp2)H)9c)MC5~+O900%j9-pNLja!xrD2M~KQOZV^!lRD~{ zj9^*r=lm~CWke9d|J4!7CBqHo{F7BghChx*Kvrxw+gSg2IA@^3jgUbIA)t^85-cF# q Date: Thu, 22 Feb 2024 11:01:38 -0700 Subject: [PATCH 2/3] Update server/certstore/certstore_windows.go Co-authored-by: Byron Ruth --- server/certstore/certstore_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/certstore/certstore_windows.go b/server/certstore/certstore_windows.go index 1093c918541..13d9548f886 100644 --- a/server/certstore/certstore_windows.go +++ b/server/certstore/certstore_windows.go @@ -359,7 +359,7 @@ func (w *winCertStore) certBySubject(subject string, storeType uint32) (*x509.Ce return w.certSearch(winFindSubjectStr, subject, winMyStore, storeType) } -// caCertBySubject matches and returns the all matching certificates of the subject field. +// caCertBySubject matches and returns all matching certificates of the subject field. // // The following locations are searched: // 1) Root (Trusted Root Certification Authorities) From 6e768e8481ea13482428c70f90c21d842721d671 Mon Sep 17 00:00:00 2001 From: Colin Sullivan Date: Thu, 22 Feb 2024 15:41:01 -0700 Subject: [PATCH 3/3] updates based on feedback Signed-off-by: Colin Sullivan --- server/certstore/certstore_other.go | 2 +- server/certstore/certstore_windows.go | 41 ++++++++++++------- server/certstore_windows_test.go | 4 +- server/opts.go | 2 +- .../certs/tlsauth/certstore/update_keys.bat | 4 -- 5 files changed, 31 insertions(+), 22 deletions(-) delete mode 100644 test/configs/certs/tlsauth/certstore/update_keys.bat diff --git a/server/certstore/certstore_other.go b/server/certstore/certstore_other.go index e6e8aee73d4..32cd7cc029a 100644 --- a/server/certstore/certstore_other.go +++ b/server/certstore/certstore_other.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 The NATS Authors +// Copyright 2022-2024 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/server/certstore/certstore_windows.go b/server/certstore/certstore_windows.go index 13d9548f886..6e6e24dc173 100644 --- a/server/certstore/certstore_windows.go +++ b/server/certstore/certstore_windows.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 The NATS Authors +// Copyright 2022-2024 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -111,7 +111,13 @@ var ( crypto.SHA512: winWide("SHA512"), // BCRYPT_SHA512_ALGORITHM } - // MY is well-known system store on Windows that holds personal certificates + // MY is well-known system store on Windows that holds personal certificates. Read + // More about the CA locations here: + // https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/wcf/certificate-of-clientcertificate-element?redirectedfrom=MSDN + // https://superuser.com/questions/217719/what-are-the-windows-system-certificate-stores + // https://docs.microsoft.com/en-us/windows/win32/seccrypto/certificate-stores + // https://learn.microsoft.com/en-us/windows/win32/seccrypto/system-store-locations + // https://stackoverflow.com/questions/63286085/which-x509-storename-refers-to-the-certificates-stored-beneath-trusted-root-cert#:~:text=4-,StoreName.,is%20%22Intermediate%20Certification%20Authorities%22. winMyStore = winWide("MY") winIntermediateCAStore = winWide("CA") winRootStore = winWide("Root") @@ -146,31 +152,33 @@ type winPSSPaddingInfo struct { // request. If no certificates are found an error is returned. func createCACertsPool(cs *winCertStore, storeType uint32, caCertsMatch []string) (*x509.CertPool, error) { var errs []error - var count int caPool := x509.NewCertPool() for _, s := range caCertsMatch { lfs, err := cs.caCertsBySubjectMatch(s, storeType) if err != nil { - if errs == nil { - errs = make([]error, 0) - } errs = append(errs, err) } else { for _, lf := range lfs { caPool.AddCert(lf) - count++ } } } - // If no certs were added, return the errors. - if count == 0 { + // If every lookup failed return the errors. + if len(errs) == len(caCertsMatch) { return nil, fmt.Errorf("unable to match any CA certificate: %v", errs) } return caPool, nil } -// TLSConfig fulfills the same function as reading cert and key pair from pem files but -// sources the Windows certificate store instead +// TLSConfig fulfills the same function as reading cert and key pair from +// pem files but sources the Windows certificate store instead. The +// certMatchBy and certMatch fields search the "MY" certificate location +// for the first certificate that matches the certMatch field. The +// caCertsMatch field is used to search the Trusted Root, Third Party Root, +// and Intermediate Certificate Authority locations for certificates with +// Subjects matching the provided strings. If a match is found, the +// certificate is added to the pool that is used to verify the certificate +// chain. func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, caCertsMatch []string, config *tls.Config) error { var ( leaf *x509.Certificate @@ -219,7 +227,7 @@ func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, c return ErrNoPrivateKeyStoreRef } // Look for CA Certificates - if caCertsMatch != nil { + if len(caCertsMatch) != 0 { caPool, err := createCACertsPool(cs, scope, caCertsMatch) if err != nil { return err @@ -372,17 +380,22 @@ func (w *winCertStore) caCertsBySubjectMatch(subject string, storeType uint32) ( var ( leaf *x509.Certificate searchLocations = [3]*uint16{winRootStore, winAuthRootStore, winIntermediateCAStore} + rv []*x509.Certificate ) // surprisingly, an empty string returns a result. We'll treat this as an error. if subject == "" { - return nil, ErrFailedCertSearch + return nil, ErrBadCaCertMatchField } - rv := make([]*x509.Certificate, 0) for _, sr := range searchLocations { var err error if leaf, _, err = w.certSearch(winFindSubjectStr, subject, sr, storeType); err == nil { rv = append(rv, leaf) } else { + // Ignore the failed search from a single location. Errors we catch include + // ErrFailedX509Extract (resulting from a malformed certificate) and errors + // around invalid attributes, unsupported algorithms, etc. These are corner + // cases as certificates with these errors shouldn't have been allowed + // to be added to the store in the first place. if err != ErrFailedCertSearch { return nil, err } diff --git a/server/certstore_windows_test.go b/server/certstore_windows_test.go index 2cb8bc69ef5..93ea441f570 100644 --- a/server/certstore_windows_test.go +++ b/server/certstore_windows_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 The NATS Authors +// Copyright 2022-2024 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -61,7 +61,7 @@ func runConfiguredLeaf(t *testing.T, hubPort int, certStore string, matchBy stri cert_match: "%s" ca_certs_match: %s - # Above should be equivalent to: + # Test settings that succeed should be equivalent to: # cert_file: "../test/configs/certs/tlsauth/client.pem" # key_file: "../test/configs/certs/tlsauth/client-key.pem" # ca_file: "../test/configs/certs/tlsauth/ca.pem" diff --git a/server/opts.go b/server/opts.go index 0df33eb0319..3368c73c9d2 100644 --- a/server/opts.go +++ b/server/opts.go @@ -1,4 +1,4 @@ -// Copyright 2012-2023 The NATS Authors +// Copyright 2012-2024 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/test/configs/certs/tlsauth/certstore/update_keys.bat b/test/configs/certs/tlsauth/certstore/update_keys.bat deleted file mode 100644 index 8b3b3df6807..00000000000 --- a/test/configs/certs/tlsauth/certstore/update_keys.bat +++ /dev/null @@ -1,4 +0,0 @@ -REM C:\Progra~1\Git\usr\bin\openssl pkcs12 -export -nokeys -in ..\ca.pem -out ca.p12 -C:\Progra~1\Git\usr\bin\openssl pkcs12 -export -inkey ..\client-key.pem -in ..\client.pem -out client.p12 -C:\Progra~1\Git\usr\bin\openssl pkcs12 -export -inkey ..\server-key.pem -in ..\server.pem -out server.p12 -C:\Progra~1\Git\usr\bin\openssl pkcs12 -export -nokeys -in ..\ca.pem -out ca.p12 \ No newline at end of file