From f854e95c1101b3894dddb7044a7eb3962136c866 Mon Sep 17 00:00:00 2001 From: Todd Beets Date: Thu, 22 Jun 2023 12:19:39 -0700 Subject: [PATCH] Cert Store (aka wincert) --- server/certstore/certstore.go | 91 ++ server/certstore/certstore_other.go | 46 + server/certstore/certstore_windows.go | 827 ++++++++++++++++++ server/certstore/errors.go | 73 ++ server/certstore_windows_test.go | 230 +++++ server/opts.go | 51 +- .../certs/tlsauth/certstore/client.p12 | Bin 0 -> 2509 bytes .../certstore/delete-cert-from-store.ps1 | 2 + .../tlsauth/certstore/import-p12-client.ps1 | 5 + .../tlsauth/certstore/import-p12-server.ps1 | 5 + .../configs/certs/tlsauth/certstore/pkcs12.md | 22 + .../certs/tlsauth/certstore/server.p12 | Bin 0 -> 2533 bytes 12 files changed, 1346 insertions(+), 6 deletions(-) create mode 100644 server/certstore/certstore.go create mode 100644 server/certstore/certstore_other.go create mode 100644 server/certstore/certstore_windows.go create mode 100644 server/certstore/errors.go create mode 100644 server/certstore_windows_test.go create mode 100644 test/configs/certs/tlsauth/certstore/client.p12 create mode 100644 test/configs/certs/tlsauth/certstore/delete-cert-from-store.ps1 create mode 100644 test/configs/certs/tlsauth/certstore/import-p12-client.ps1 create mode 100644 test/configs/certs/tlsauth/certstore/import-p12-server.ps1 create mode 100644 test/configs/certs/tlsauth/certstore/pkcs12.md create mode 100644 test/configs/certs/tlsauth/certstore/server.p12 diff --git a/server/certstore/certstore.go b/server/certstore/certstore.go new file mode 100644 index 00000000..e6195b48 --- /dev/null +++ b/server/certstore/certstore.go @@ -0,0 +1,91 @@ +// Copyright 2022-2023 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 +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certstore + +import ( + "crypto" + "io" + "runtime" + "strings" +) + +type StoreType int + +const MATCHBYEMPTY = 0 +const STOREEMPTY = 0 + +const ( + windowsCurrentUser StoreType = iota + 1 + windowsLocalMachine +) + +var StoreMap = map[string]StoreType{ + "windowscurrentuser": windowsCurrentUser, + "windowslocalmachine": windowsLocalMachine, +} + +var StoreOSMap = map[StoreType]string{ + windowsCurrentUser: "windows", + windowsLocalMachine: "windows", +} + +type MatchByType int + +const ( + matchByIssuer MatchByType = iota + 1 + matchBySubject +) + +var MatchByMap = map[string]MatchByType{ + "issuer": matchByIssuer, + "subject": matchBySubject, +} + +var Usage = ` +In place of cert_file and key_file you may use the windows certificate store: + + tls { + cert_store: "WindowsCurrentUser" + cert_match_by: "Subject" + cert_match: "MyServer123" + } +` + +func ParseCertStore(certStore string) (StoreType, error) { + certStoreType, exists := StoreMap[strings.ToLower(certStore)] + if !exists { + return 0, ErrBadCertStore + } + validOS, exists := StoreOSMap[certStoreType] + if !exists || validOS != runtime.GOOS { + return 0, ErrOSNotCompatCertStore + } + return certStoreType, nil +} + +func ParseCertMatchBy(certMatchBy string) (MatchByType, error) { + certMatchByType, exists := MatchByMap[strings.ToLower(certMatchBy)] + if !exists { + return 0, ErrBadMatchByType + } + return certMatchByType, nil +} + +// credential provides access to a public key and is a crypto.Signer. +type credential interface { + // Public returns the public key corresponding to the leaf certificate. + Public() crypto.PublicKey + // Sign signs digest with the private key. + Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) +} diff --git a/server/certstore/certstore_other.go b/server/certstore/certstore_other.go new file mode 100644 index 00000000..a72df834 --- /dev/null +++ b/server/certstore/certstore_other.go @@ -0,0 +1,46 @@ +// Copyright 2022-2023 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 +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows + +package certstore + +import ( + "crypto" + "crypto/tls" + "io" +) + +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 + return ErrOSNotCompatCertStore +} + +// Public always returns nil public key since this is a stub on non-supported platform +func (k otherKey) Public() crypto.PublicKey { + return nil +} + +// Sign always returns a nil signature since this is a stub on non-supported platform +func (k otherKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + _, _, _ = rand, digest, opts + return nil, nil +} + +// Verify interface conformance. +var _ credential = &otherKey{} diff --git a/server/certstore/certstore_windows.go b/server/certstore/certstore_windows.go new file mode 100644 index 00000000..57adc187 --- /dev/null +++ b/server/certstore/certstore_windows.go @@ -0,0 +1,827 @@ +// Copyright 2022-2023 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 +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted, updated, and enhanced from CertToStore, https://github.com/google/certtostore/releases/tag/v1.0.2 +// Apache License, Version 2.0, Copyright 2017 Google Inc. + +package certstore + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/binary" + "fmt" + "io" + "math/big" + "reflect" + "sync" + "syscall" + "unicode/utf16" + "unsafe" + + "golang.org/x/crypto/cryptobyte" + "golang.org/x/crypto/cryptobyte/asn1" + "golang.org/x/sys/windows" +) + +const ( + // wincrypt.h constants + winAcquireCached = 0x1 // CRYPT_ACQUIRE_CACHE_FLAG + winAcquireSilent = 0x40 // CRYPT_ACQUIRE_SILENT_FLAG + winAcquireOnlyNCryptKey = 0x40000 // CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG + winEncodingX509ASN = 1 // X509_ASN_ENCODING + winEncodingPKCS7 = 65536 // PKCS_7_ASN_ENCODING + winCertStoreProvSystem = 10 // CERT_STORE_PROV_SYSTEM + winCertStoreCurrentUser = uint32(winCertStoreCurrentUserID << winCompareShift) // CERT_SYSTEM_STORE_CURRENT_USER + winCertStoreLocalMachine = uint32(winCertStoreLocalMachineID << winCompareShift) // CERT_SYSTEM_STORE_LOCAL_MACHINE + winCertStoreCurrentUserID = 1 // CERT_SYSTEM_STORE_CURRENT_USER_ID + winCertStoreLocalMachineID = 2 // CERT_SYSTEM_STORE_LOCAL_MACHINE_ID + winInfoIssuerFlag = 4 // CERT_INFO_ISSUER_FLAG + winInfoSubjectFlag = 7 // CERT_INFO_SUBJECT_FLAG + winCompareNameStrW = 8 // CERT_COMPARE_NAME_STR_A + winCompareShift = 16 // CERT_COMPARE_SHIFT + + // Reference https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore + winFindIssuerStr = winCompareNameStrW< 1 { + chain = chain[:len(chain)-1] + } + + // For tls.Certificate.Certificate need a [][]byte from []*x509.Certificate + // Approximate capacity for efficiency + rawChain = make([][]byte, 0, len(chain)) + for _, link := range chain { + rawChain = append(rawChain, link.Raw) + } + + tlsCert := tls.Certificate{ + Certificate: rawChain, + PrivateKey: pk, + Leaf: leaf, + } + config.Certificates = []tls.Certificate{tlsCert} + + // note: pk is a windows pointer (not freed by Go) but needs to live the life of the server for Signing. + // The cert context (leafCtx) windows pointer must not be freed underneath the pk so also life of the server. + return nil +} + +// winWide returns a pointer to uint16 representing the equivalent +// to a Windows LPCWSTR. +func winWide(s string) *uint16 { + w := utf16.Encode([]rune(s)) + w = append(w, 0) + return &w[0] +} + +// winOpenProvider gets a provider handle for subsequent calls +func winOpenProvider(provider string) (uintptr, error) { + var hProv uintptr + pname := winWide(provider) + // Open the provider, the last parameter is not used + r, _, err := winNCryptOpenStorageProvider.Call(uintptr(unsafe.Pointer(&hProv)), uintptr(unsafe.Pointer(pname)), 0) + if r == 0 { + return hProv, nil + } + return hProv, fmt.Errorf("NCryptOpenStorageProvider returned %X: %v", r, err) +} + +// winFindCert wraps the CertFindCertificateInStore library call. Note that any cert context passed +// into prev will be freed. If no certificate was found, nil will be returned. +func winFindCert(store windows.Handle, enc, findFlags, findType uint32, para *uint16, prev *windows.CertContext) (*windows.CertContext, error) { + h, _, err := winCertFindCertificateInStore.Call( + uintptr(store), + uintptr(enc), + uintptr(findFlags), + uintptr(findType), + uintptr(unsafe.Pointer(para)), + uintptr(unsafe.Pointer(prev)), + ) + if h == 0 { + // Actual error, or simply not found? + if errno, ok := err.(syscall.Errno); ok && errno == winCryptENotFound { + return nil, ErrFailedCertSearch + } + return nil, ErrFailedCertSearch + } + // nolint:govet + return (*windows.CertContext)(unsafe.Pointer(h)), nil +} + +// winCertStore is a store implementation for the Windows Certificate Store +type winCertStore struct { + Prov uintptr + ProvName string + stores map[string]*winStoreHandle + mu sync.Mutex +} + +// winOpenCertStore creates a winCertStore +func winOpenCertStore(provider string) (*winCertStore, error) { + cngProv, err := winOpenProvider(provider) + if err != nil { + // pass through error from winOpenProvider + return nil, err + } + + wcs := &winCertStore{ + Prov: cngProv, + ProvName: provider, + stores: make(map[string]*winStoreHandle), + } + + return wcs, nil +} + +// winCertContextToX509 creates an x509.Certificate from a Windows cert context. +func winCertContextToX509(ctx *windows.CertContext) (*x509.Certificate, error) { + var der []byte + slice := (*reflect.SliceHeader)(unsafe.Pointer(&der)) + slice.Data = uintptr(unsafe.Pointer(ctx.EncodedCert)) + slice.Len = int(ctx.Length) + slice.Cap = int(ctx.Length) + return x509.ParseCertificate(der) +} + +// certByIssuer matches and returns the first certificate found by passed issuer. +// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies +// current user's personal certs or local machine's personal certs using storeType. +// See CERT_FIND_ISSUER_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore +func (w *winCertStore) certByIssuer(issuer string, storeType uint32) (*x509.Certificate, *windows.CertContext, error) { + return w.certSearch(winFindIssuerStr, issuer, winMyStore, storeType) +} + +// certBySubject matches and returns the first certificate found by passed subject field. +// CertContext pointer returned allows subsequent key operations like Sign. 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) certBySubject(subject string, storeType uint32) (*x509.Certificate, *windows.CertContext, error) { + return w.certSearch(winFindSubjectStr, subject, winMyStore, storeType) +} + +// 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) { + // store handle to "MY" store + h, err := w.storeHandle(store, searchRoot) + if err != nil { + return nil, nil, err + } + + var prev *windows.CertContext + var cert *x509.Certificate + + i, err := windows.UTF16PtrFromString(matchValue) + if err != nil { + return nil, nil, ErrFailedCertSearch + } + + // pass 0 as the third parameter because it is not used + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa376064(v=vs.85).aspx + nc, err := winFindCert(h, winEncodingX509ASN|winEncodingPKCS7, 0, searchType, i, prev) + if err != nil { + return nil, nil, err + } + if nc != nil { + // certificate found + prev = nc + + // Extract the DER-encoded certificate from the cert context + xc, err := winCertContextToX509(nc) + if err == nil { + cert = xc + } else { + return nil, nil, ErrFailedX509Extract + } + } else { + return nil, nil, ErrFailedCertSearch + } + + if cert == nil { + return nil, nil, ErrFailedX509Extract + } + + return cert, prev, nil +} + +type winStoreHandle struct { + handle *windows.Handle +} + +func winNewStoreHandle(provider uint32, store *uint16) (*winStoreHandle, error) { + var s winStoreHandle + if s.handle != nil { + return &s, nil + } + st, err := windows.CertOpenStore( + winCertStoreProvSystem, + 0, + 0, + provider, + uintptr(unsafe.Pointer(store))) + if err != nil { + return nil, ErrBadCryptoStoreProvider + } + s.handle = &st + return &s, nil +} + +// winKey implements crypto.Signer and crypto.Decrypter for key based operations. +type winKey struct { + handle uintptr + pub crypto.PublicKey + Container string + AlgorithmGroup string +} + +// Public exports a public key to implement crypto.Signer +func (k winKey) Public() crypto.PublicKey { + return k.pub +} + +// Sign returns the signature of a hash to implement crypto.Signer +func (k winKey) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + switch k.AlgorithmGroup { + case "ECDSA", "ECDH": + return winSignECDSA(k.handle, digest) + case "RSA": + hf := opts.HashFunc() + algID, ok := winAlgIDs[hf] + if !ok { + return nil, ErrBadRSAHashAlgorithm + } + switch opts.(type) { + case *rsa.PSSOptions: + return winSignRSAPSSPadding(k.handle, digest, algID) + default: + return winSignRSAPKCS1Padding(k.handle, digest, algID) + } + default: + return nil, ErrBadSigningAlgorithm + } +} + +func winSignECDSA(kh uintptr, digest []byte) ([]byte, error) { + var size uint32 + // Obtain the size of the signature + r, _, _ := winNCryptSignHash.Call( + kh, + 0, + uintptr(unsafe.Pointer(&digest[0])), + uintptr(len(digest)), + 0, + 0, + uintptr(unsafe.Pointer(&size)), + 0) + if r != 0 { + return nil, ErrStoreECDSASigningError + } + + // Obtain the signature data + buf := make([]byte, size) + r, _, _ = winNCryptSignHash.Call( + kh, + 0, + uintptr(unsafe.Pointer(&digest[0])), + uintptr(len(digest)), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(size), + uintptr(unsafe.Pointer(&size)), + 0) + if r != 0 { + return nil, ErrStoreECDSASigningError + } + if len(buf) != int(size) { + return nil, ErrStoreECDSASigningError + } + + return winPackECDSASigValue(bytes.NewReader(buf[:size]), len(digest)) +} + +func winPackECDSASigValue(r io.Reader, digestLength int) ([]byte, error) { + sigR := make([]byte, digestLength) + if _, err := io.ReadFull(r, sigR); err != nil { + return nil, ErrStoreECDSASigningError + } + + sigS := make([]byte, digestLength) + if _, err := io.ReadFull(r, sigS); err != nil { + return nil, ErrStoreECDSASigningError + } + + var b cryptobyte.Builder + b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) { + b.AddASN1BigInt(new(big.Int).SetBytes(sigR)) + b.AddASN1BigInt(new(big.Int).SetBytes(sigS)) + }) + return b.Bytes() +} + +func winSignRSAPKCS1Padding(kh uintptr, digest []byte, algID *uint16) ([]byte, error) { + // PKCS#1 v1.5 padding for some TLS 1.2 + padInfo := winPKCS1PaddingInfo{pszAlgID: algID} + var size uint32 + // Obtain the size of the signature + r, _, _ := winNCryptSignHash.Call( + kh, + uintptr(unsafe.Pointer(&padInfo)), + uintptr(unsafe.Pointer(&digest[0])), + uintptr(len(digest)), + 0, + 0, + uintptr(unsafe.Pointer(&size)), + winBCryptPadPKCS1) + if r != 0 { + return nil, ErrStoreRSASigningError + } + + // Obtain the signature data + sig := make([]byte, size) + r, _, _ = winNCryptSignHash.Call( + kh, + uintptr(unsafe.Pointer(&padInfo)), + uintptr(unsafe.Pointer(&digest[0])), + uintptr(len(digest)), + uintptr(unsafe.Pointer(&sig[0])), + uintptr(size), + uintptr(unsafe.Pointer(&size)), + winBCryptPadPKCS1) + if r != 0 { + return nil, ErrStoreRSASigningError + } + + return sig[:size], nil +} + +func winSignRSAPSSPadding(kh uintptr, digest []byte, algID *uint16) ([]byte, error) { + // PSS padding for TLS 1.3 and some TLS 1.2 + padInfo := winPSSPaddingInfo{pszAlgID: algID, cbSalt: winBCryptPadPSSSalt} + + var size uint32 + // Obtain the size of the signature + r, _, _ := winNCryptSignHash.Call( + kh, + uintptr(unsafe.Pointer(&padInfo)), + uintptr(unsafe.Pointer(&digest[0])), + uintptr(len(digest)), + 0, + 0, + uintptr(unsafe.Pointer(&size)), + winBCryptPadPSS) + if r != 0 { + return nil, ErrStoreRSASigningError + } + + // Obtain the signature data + sig := make([]byte, size) + r, _, _ = winNCryptSignHash.Call( + kh, + uintptr(unsafe.Pointer(&padInfo)), + uintptr(unsafe.Pointer(&digest[0])), + uintptr(len(digest)), + uintptr(unsafe.Pointer(&sig[0])), + uintptr(size), + uintptr(unsafe.Pointer(&size)), + winBCryptPadPSS) + if r != 0 { + return nil, ErrStoreRSASigningError + } + + return sig[:size], nil +} + +// certKey wraps CryptAcquireCertificatePrivateKey. It obtains the CNG private +// key of a known certificate and returns a pointer to a winKey which implements +// both crypto.Signer. When a nil cert context is passed +// a nil key is intentionally returned, to model the expected behavior of a +// non-existent cert having no private key. +// https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptacquirecertificateprivatekey +func (w *winCertStore) certKey(cert *windows.CertContext) (*winKey, error) { + // Return early if a nil cert was passed. + if cert == nil { + return nil, nil + } + var ( + kh uintptr + spec uint32 + mustFree int + ) + r, _, _ := winCryptAcquireCertificatePrivateKey.Call( + uintptr(unsafe.Pointer(cert)), + winAcquireCached|winAcquireSilent|winAcquireOnlyNCryptKey, + 0, // Reserved, must be null. + uintptr(unsafe.Pointer(&kh)), + uintptr(unsafe.Pointer(&spec)), + uintptr(unsafe.Pointer(&mustFree)), + ) + // If the function succeeds, the return value is nonzero (TRUE). + if r == 0 { + return nil, ErrNoPrivateKeyStoreRef + } + if mustFree != 0 { + return nil, ErrNoPrivateKeyStoreRef + } + if spec != winNcryptKeySpec { + return nil, ErrNoPrivateKeyStoreRef + } + + return winKeyMetadata(kh) +} + +func winKeyMetadata(kh uintptr) (*winKey, error) { + // uc is used to populate the unique container name attribute of the private key + uc, err := winGetPropertyStr(kh, winNCryptUniqueNameProperty) + if err != nil { + // unable to determine key unique name + return nil, ErrExtractingPrivateKeyMetadata + } + + alg, err := winGetPropertyStr(kh, winNCryptAlgorithmGroupProperty) + if err != nil { + // unable to determine key algorithm + return nil, ErrExtractingPrivateKeyMetadata + } + + var pub crypto.PublicKey + + switch alg { + case "ECDSA", "ECDH": + buf, err := winExport(kh, winBCryptECCPublicBlob) + if err != nil { + // failed to export ECC public key + return nil, ErrExtractingECCPublicKey + } + pub, err = unmarshalECC(buf, kh) + if err != nil { + return nil, ErrExtractingECCPublicKey + } + case "RSA": + buf, err := winExport(kh, winBCryptRSAPublicBlob) + if err != nil { + return nil, ErrExtractingRSAPublicKey + } + pub, err = winUnmarshalRSA(buf) + if err != nil { + return nil, ErrExtractingRSAPublicKey + } + default: + return nil, ErrBadPublicKeyAlgorithm + } + + return &winKey{handle: kh, pub: pub, Container: uc, AlgorithmGroup: alg}, nil +} + +func winGetProperty(kh uintptr, property *uint16) ([]byte, error) { + var strSize uint32 + r, _, _ := winNCryptGetProperty.Call( + kh, + uintptr(unsafe.Pointer(property)), + 0, + 0, + uintptr(unsafe.Pointer(&strSize)), + 0, + 0) + if r != 0 { + return nil, ErrExtractPropertyFromKey + } + + buf := make([]byte, strSize) + r, _, _ = winNCryptGetProperty.Call( + kh, + uintptr(unsafe.Pointer(property)), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(strSize), + uintptr(unsafe.Pointer(&strSize)), + 0, + 0) + if r != 0 { + return nil, ErrExtractPropertyFromKey + } + + return buf, nil +} + +func winGetPropertyStr(kh uintptr, property *uint16) (string, error) { + buf, err := winFnGetProperty(kh, property) + if err != nil { + return "", ErrExtractPropertyFromKey + } + uc := bytes.ReplaceAll(buf, []byte{0x00}, []byte("")) + return string(uc), nil +} + +func winExport(kh uintptr, blobType *uint16) ([]byte, error) { + var size uint32 + // When obtaining the size of a public key, most parameters are not required + r, _, _ := winNCryptExportKey.Call( + kh, + 0, + uintptr(unsafe.Pointer(blobType)), + 0, + 0, + 0, + uintptr(unsafe.Pointer(&size)), + 0) + if r != 0 { + return nil, ErrExtractingPublicKey + } + + // Place the exported key in buf now that we know the size required + buf := make([]byte, size) + r, _, _ = winNCryptExportKey.Call( + kh, + 0, + uintptr(unsafe.Pointer(blobType)), + 0, + uintptr(unsafe.Pointer(&buf[0])), + uintptr(size), + uintptr(unsafe.Pointer(&size)), + 0) + if r != 0 { + return nil, ErrExtractingPublicKey + } + return buf, nil +} + +func unmarshalECC(buf []byte, kh uintptr) (*ecdsa.PublicKey, error) { + // BCRYPT_ECCKEY_BLOB from bcrypt.h + header := struct { + Magic uint32 + Key uint32 + }{} + + r := bytes.NewReader(buf) + if err := binary.Read(r, binary.LittleEndian, &header); err != nil { + return nil, ErrExtractingECCPublicKey + } + + curve, ok := winCurveIDs[header.Magic] + if !ok { + // Fix for b/185945636, where despite specifying the curve, nCrypt returns + // an incorrect response with BCRYPT_ECDSA_PUBLIC_GENERIC_MAGIC. + var err error + curve, err = winCurveName(kh) + if err != nil { + // unsupported header magic or cannot match the curve by name + return nil, err + } + } + + keyX := make([]byte, header.Key) + if n, err := r.Read(keyX); n != int(header.Key) || err != nil { + // failed to read key X + return nil, ErrExtractingECCPublicKey + } + + keyY := make([]byte, header.Key) + if n, err := r.Read(keyY); n != int(header.Key) || err != nil { + // failed to read key Y + return nil, ErrExtractingECCPublicKey + } + + pub := &ecdsa.PublicKey{ + Curve: curve, + X: new(big.Int).SetBytes(keyX), + Y: new(big.Int).SetBytes(keyY), + } + return pub, nil +} + +// winCurveName reads the curve name property and returns the corresponding curve. +func winCurveName(kh uintptr) (elliptic.Curve, error) { + cn, err := winGetPropertyStr(kh, winNCryptECCCurveNameProperty) + if err != nil { + // unable to determine the curve property name + return nil, ErrExtractPropertyFromKey + } + curve, ok := winCurveNames[cn] + if !ok { + // unknown curve name + return nil, ErrBadECCCurveName + } + return curve, nil +} + +func winUnmarshalRSA(buf []byte) (*rsa.PublicKey, error) { + // BCRYPT_RSA_BLOB from bcrypt.h + header := struct { + Magic uint32 + BitLength uint32 + PublicExpSize uint32 + ModulusSize uint32 + UnusedPrime1 uint32 + UnusedPrime2 uint32 + }{} + + r := bytes.NewReader(buf) + if err := binary.Read(r, binary.LittleEndian, &header); err != nil { + return nil, ErrExtractingRSAPublicKey + } + + if header.Magic != winRSA1Magic { + // invalid header magic + return nil, ErrExtractingRSAPublicKey + } + + if header.PublicExpSize > 8 { + // unsupported public exponent size + return nil, ErrExtractingRSAPublicKey + } + + exp := make([]byte, 8) + if n, err := r.Read(exp[8-header.PublicExpSize:]); n != int(header.PublicExpSize) || err != nil { + // failed to read public exponent + return nil, ErrExtractingRSAPublicKey + } + + mod := make([]byte, header.ModulusSize) + if n, err := r.Read(mod); n != int(header.ModulusSize) || err != nil { + // failed to read modulus + return nil, ErrExtractingRSAPublicKey + } + + pub := &rsa.PublicKey{ + N: new(big.Int).SetBytes(mod), + E: int(binary.BigEndian.Uint64(exp)), + } + return pub, nil +} + +// storeHandle returns a handle to a given cert store, opening the handle as needed. +func (w *winCertStore) storeHandle(provider uint32, store *uint16) (windows.Handle, error) { + w.mu.Lock() + defer w.mu.Unlock() + + key := fmt.Sprintf("%d%s", provider, windows.UTF16PtrToString(store)) + var err error + if w.stores[key] == nil { + w.stores[key], err = winNewStoreHandle(provider, store) + if err != nil { + return 0, ErrBadCryptoStoreProvider + } + } + return *w.stores[key].handle, nil +} + +// Verify interface conformance. +var _ credential = &winKey{} diff --git a/server/certstore/errors.go b/server/certstore/errors.go new file mode 100644 index 00000000..bbb1c9d8 --- /dev/null +++ b/server/certstore/errors.go @@ -0,0 +1,73 @@ +package certstore + +import ( + "errors" +) + +var ( + // ErrBadCryptoStoreProvider represents inablity to establish link with a certificate store + ErrBadCryptoStoreProvider = errors.New("unable to open certificate store or store not available") + + // ErrBadRSAHashAlgorithm represents a bad or unsupported RSA hash algorithm + ErrBadRSAHashAlgorithm = errors.New("unsupported RSA hash algorithm") + + // ErrBadSigningAlgorithm represents a bad or unsupported signing algorithm + ErrBadSigningAlgorithm = errors.New("unsupported signing algorithm") + + // ErrStoreRSASigningError represents an error returned from store during RSA signature + ErrStoreRSASigningError = errors.New("unable to obtain RSA signature from store") + + // ErrStoreECDSASigningError represents an error returned from store during ECDSA signature + ErrStoreECDSASigningError = errors.New("unable to obtain ECDSA signature from store") + + // ErrNoPrivateKeyStoreRef represents an error getting a handle to a private key in store + ErrNoPrivateKeyStoreRef = errors.New("unable to obtain private key handle from store") + + // ErrExtractingPrivateKeyMetadata represents a family of errors extracting metadata about the private key in store + ErrExtractingPrivateKeyMetadata = errors.New("unable to extract private key metadata") + + // ErrExtractingECCPublicKey represents an error exporting ECC-type public key from store + ErrExtractingECCPublicKey = errors.New("unable to extract ECC public key from store") + + // ErrExtractingRSAPublicKey represents an error exporting RSA-type public key from store + ErrExtractingRSAPublicKey = errors.New("unable to extract RSA public key from store") + + // ErrExtractingPublicKey represents a general error exporting public key from store + ErrExtractingPublicKey = errors.New("unable to extract public key from store") + + // ErrBadPublicKeyAlgorithm represents a bad or unsupported public key algorithm + ErrBadPublicKeyAlgorithm = errors.New("unsupported public key algorithm") + + // ErrExtractPropertyFromKey represents a general failure to extract a metadata property field + ErrExtractPropertyFromKey = errors.New("unable to extract property from key") + + // ErrBadECCCurveName represents an ECC signature curve name that is bad or unsupported + ErrBadECCCurveName = errors.New("unsupported ECC curve name") + + // ErrFailedCertSearch represents not able to find certificate in store + ErrFailedCertSearch = errors.New("unable to find certificate in store") + + // ErrFailedX509Extract represents not being able to extract x509 certificate from found cert in store + ErrFailedX509Extract = errors.New("unable to extract x509 from certificate") + + // ErrBadMatchByType represents unknown CERT_MATCH_BY passed + ErrBadMatchByType = errors.New("cert match by type not implemented") + + // ErrBadCertStore represents unknown CERT_STORE passed + ErrBadCertStore = errors.New("cert store type not implemented") + + // ErrConflictCertFileAndStore represents ambiguous configuration of both file and store + ErrConflictCertFileAndStore = errors.New("'cert_file' and 'cert_store' may not both be configured") + + // ErrBadCertStoreField represents malformed cert_store option + ErrBadCertStoreField = errors.New("expected 'cert_store' to be a valid non-empty string") + + // ErrBadCertMatchByField represents malformed cert_match_by option + ErrBadCertMatchByField = errors.New("expected 'cert_match_by' to be a valid non-empty string") + + // ErrBadCertMatchField represents malformed cert_match option + ErrBadCertMatchField = errors.New("expected 'cert_match' to be a valid non-empty string") + + // 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 new file mode 100644 index 00000000..e0f33c2e --- /dev/null +++ b/server/certstore_windows_test.go @@ -0,0 +1,230 @@ +// Copyright 2022-2023 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 +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows + +package server + +import ( + "fmt" + "net/url" + "os" + "os/exec" + "strings" + "testing" + "time" + + "github.com/nats-io/nats.go" +) + +func runPowershellScript(scriptFile string, args []string) error { + _ = args + psExec, _ := exec.LookPath("powershell.exe") + execArgs := []string{psExec, "-command", fmt.Sprintf("& '%s'", scriptFile)} + + cmdImport := &exec.Cmd{ + Path: psExec, + Args: execArgs, + Stdout: os.Stdout, + Stderr: os.Stderr, + } + return cmdImport.Run() +} + +func runConfiguredLeaf(t *testing.T, hubPort int, certStore string, matchBy string, match string, expectedLeafCount int) { + + // Fire up the leaf + u, err := url.Parse(fmt.Sprintf("nats://localhost:%d", hubPort)) + if err != nil { + t.Fatalf("Error parsing url: %v", err) + } + + configStr := fmt.Sprintf(` + port: -1 + leaf { + remotes [ + { + url: "%s" + tls { + cert_store: "%s" + cert_match_by: "%s" + cert_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" + timeout: 5 + } + } + ] + } + `, u.String(), certStore, matchBy, match) + + leafConfig := createConfFile(t, []byte(configStr)) + defer removeFile(t, leafConfig) + leafServer, _ := RunServerWithConfig(leafConfig) + defer leafServer.Shutdown() + + // After client verify, hub will match by SAN email, SAN dns, and Subject (in that order) + // Our test client specifies Subject only so we should match on that... + + // A little settle time + time.Sleep(1 * time.Second) + checkLeafNodeConnectedCount(t, leafServer, expectedLeafCount) +} + +// TestLeafTLSWindowsCertStore tests the topology of two NATS Servers connected as leaf and hub with authentication of +// leaf to hub via mTLS with leaf's certificate and signing key provisioned in the Windows certificate store. +func TestLeafTLSWindowsCertStore(t *testing.T) { + + // Client Identity (client.pem) + // Issuer: O = Synadia Communications Inc., 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 + err := runPowershellScript("../test/configs/certs/tlsauth/certstore/delete-cert-from-store.ps1", nil) + if err != nil { + t.Fatalf("expected powershell cert delete to succeed: %s", err.Error()) + } + + // Provision Windows cert store with client cert and secret + err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-client.ps1", nil) + if err != nil { + t.Fatalf("expected powershell provision to succeed: %s", err.Error()) + } + + // Fire up the hub + hubConfig := createConfFile(t, []byte(` + port: -1 + leaf { + listen: "127.0.0.1:-1" + tls { + ca_file: "../test/configs/certs/tlsauth/ca.pem" + cert_file: "../test/configs/certs/tlsauth/server.pem" + key_file: "../test/configs/certs/tlsauth/server-key.pem" + timeout: 5 + verify_and_map: true + } + } + + accounts: { + AcctA: { + users: [ {user: "OU = NATS.io, CN = example.com"} ] + }, + AcctB: { + users: [ {user: UserB1} ] + }, + SYS: { + users: [ {user: System} ] + } + } + system_account: "SYS" + `)) + defer removeFile(t, hubConfig) + hubServer, hubOptions := RunServerWithConfig(hubConfig) + defer hubServer.Shutdown() + + testCases := []struct { + certStore string + certMatchBy string + certMatch string + expectedLeafCount int + }{ + {"WindowsCurrentUser", "Subject", "example.com", 1}, + {"WindowsCurrentUser", "Issuer", "Synadia Communications Inc.", 1}, + {"WindowsCurrentUser", "Issuer", "Frodo Baggins, Inc.", 0}, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s by %s match %s", tc.certStore, tc.certMatchBy, tc.certMatch), func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if tc.expectedLeafCount != 0 { + t.Fatalf("did not expect panic") + } else { + if !strings.Contains(fmt.Sprintf("%v", r), "Error processing configuration file") { + t.Fatalf("did not expect unknown panic cause") + } + } + } + }() + runConfiguredLeaf(t, hubOptions.LeafNode.Port, tc.certStore, tc.certMatchBy, tc.certMatch, tc.expectedLeafCount) + }) + } +} + +// TestServerTLSWindowsCertStore tests the topology of a NATS server requiring TLS and gettings it own server +// cert identiy (as used when accepting NATS client connections and negotiating TLS) from Windows certificate store. +func TestServerTLSWindowsCertStore(t *testing.T) { + + // Server Identity (server.pem) + // Issuer: O = Synadia Communications Inc., 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 + err := runPowershellScript("../test/configs/certs/tlsauth/certstore/delete-cert-from-store.ps1", nil) + if err != nil { + t.Fatalf("expected powershell cert delete to succeed: %s", err.Error()) + } + + // Provision Windows cert store with server cert and secret + err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-server.ps1", nil) + if err != nil { + t.Fatalf("expected powershell provision to succeed: %s", err.Error()) + } + + // Fire up the server + srvConfig := createConfFile(t, []byte(` + listen: "localhost:-1" + tls { + cert_store: "WindowsCurrentUser" + cert_match_by: "Subject" + cert_match: "NATS.io Operators" + timeout: 5 + } + `)) + defer removeFile(t, srvConfig) + srvServer, _ := RunServerWithConfig(srvConfig) + if srvServer == nil { + t.Fatalf("expected to be able start server with cert store configuration") + } + defer srvServer.Shutdown() + + testCases := []struct { + clientCA string + expect bool + }{ + {"../test/configs/certs/tlsauth/ca.pem", true}, + {"../test/configs/certs/tlsauth/client.pem", false}, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("Client CA: %s", tc.clientCA), func(t *testing.T) { + nc, _ := nats.Connect(srvServer.clientConnectURLs[0], nats.RootCAs(tc.clientCA)) + err := nc.Publish("foo", []byte("hello TLS server-authenticated server")) + if (err != nil) == tc.expect { + t.Fatalf("expected publish result %v to TLS authenticated server", tc.expect) + } + nc.Close() + + for i := 0; i < 5; i++ { + nc, _ = nats.Connect(srvServer.clientConnectURLs[0], nats.RootCAs(tc.clientCA)) + err = nc.Publish("foo", []byte("hello TLS server-authenticated server")) + if (err != nil) == tc.expect { + t.Fatalf("expected repeated connection result %v to TLS authenticated server", tc.expect) + } + nc.Close() + } + }) + } +} diff --git a/server/opts.go b/server/opts.go index 7c34bc21..4e16544c 100644 --- a/server/opts.go +++ b/server/opts.go @@ -34,9 +34,9 @@ import ( "time" "github.com/nats-io/jwt/v2" - "github.com/nats-io/nkeys" - "github.com/nats-io/nats-server/v2/conf" + "github.com/nats-io/nats-server/v2/server/certstore" + "github.com/nats-io/nkeys" ) var allowUnknownTopLevelField = int32(0) @@ -53,7 +53,7 @@ func NoErrOnUnknownFields(noError bool) { atomic.StoreInt32(&allowUnknownTopLevelField, val) } -// Set of lower case hex-encoded sha256 of DER encoded SubjectPublicKeyInfo +// PinnedCertSet is a set of lower case hex-encoded sha256 of DER encoded SubjectPublicKeyInfo type PinnedCertSet map[string]struct{} // ClusterOpts are options for clusters. @@ -574,6 +574,9 @@ type TLSConfigOpts struct { Ciphers []uint16 CurvePreferences []tls.CurveID PinnedCerts PinnedCertSet + CertStore certstore.StoreType + CertMatchBy certstore.MatchByType + CertMatch string } // OCSPConfig represents the options of OCSP stapling options. @@ -3847,6 +3850,9 @@ func PrintTLSHelpAndDie() { for k := range curvePreferenceMap { fmt.Printf(" %s\n", k) } + if runtime.GOOS == "windows" { + fmt.Printf("%s", certstore.Usage) + } os.Exit(0) } @@ -4004,6 +4010,32 @@ func parseTLS(v interface{}, isClientCtx bool) (t *TLSConfigOpts, retErr error) } tc.PinnedCerts = wl } + case "cert_store": + certStore, ok := mv.(string) + if !ok || certStore == _EMPTY_ { + return nil, &configErr{tk, certstore.ErrBadCertStoreField.Error()} + } + certStoreType, err := certstore.ParseCertStore(certStore) + if err != nil { + return nil, &configErr{tk, err.Error()} + } + tc.CertStore = certStoreType + case "cert_match_by": + certMatchBy, ok := mv.(string) + if !ok || certMatchBy == _EMPTY_ { + return nil, &configErr{tk, certstore.ErrBadCertMatchByField.Error()} + } + certMatchByType, err := certstore.ParseCertMatchBy(certMatchBy) + if err != nil { + return nil, &configErr{tk, err.Error()} + } + tc.CertMatchBy = certMatchByType + case "cert_match": + certMatch, ok := mv.(string) + if !ok || certMatch == _EMPTY_ { + return nil, &configErr{tk, certstore.ErrBadCertMatchField.Error()} + } + tc.CertMatch = certMatch default: return nil, &configErr{tk, fmt.Sprintf("error parsing tls config, unknown field [%q]", mk)} } @@ -4290,11 +4322,13 @@ func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) { } switch { - case tc.CertFile != "" && tc.KeyFile == "": + case tc.CertFile != _EMPTY_ && tc.CertStore != certstore.STOREEMPTY: + return nil, certstore.ErrConflictCertFileAndStore + case tc.CertFile != _EMPTY_ && tc.KeyFile == _EMPTY_: return nil, fmt.Errorf("missing 'key_file' in TLS configuration") - case tc.CertFile == "" && tc.KeyFile != "": + case tc.CertFile == _EMPTY_ && tc.KeyFile != _EMPTY_: return nil, fmt.Errorf("missing 'cert_file' in TLS configuration") - case tc.CertFile != "" && tc.KeyFile != "": + case tc.CertFile != _EMPTY_ && tc.KeyFile != _EMPTY_: // Now load in cert and private key cert, err := tls.LoadX509KeyPair(tc.CertFile, tc.KeyFile) if err != nil { @@ -4305,6 +4339,11 @@ func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) { return nil, fmt.Errorf("error parsing certificate: %v", err) } config.Certificates = []tls.Certificate{cert} + case tc.CertStore != certstore.STOREEMPTY: + err := certstore.TLSConfig(tc.CertStore, tc.CertMatchBy, tc.CertMatch, &config) + if err != nil { + return nil, err + } } // Require client certificates as needed diff --git a/test/configs/certs/tlsauth/certstore/client.p12 b/test/configs/certs/tlsauth/certstore/client.p12 new file mode 100644 index 0000000000000000000000000000000000000000..18ee5c32f01bd2c89fdff8779f6b309242426da8 GIT binary patch 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 + +Windows cert store supports p12/pfx bundle for certificate-with-key import. Windows cert store tests will execute +a Powershell script to import relevant PKCS12 bundle into the Windows store before the test. Equivalent to: + +`powershell.exe -command "& '..\test\configs\certs\tlsauth\certstore\import--p12.ps1'"` + +The `delete-cert-from-store.ps1` script deletes imported certificates from the Windows store (if present) that can +cause side-effects and impact the validity of different use tests. + +> Note: Tests are configured for "current user" store context. Execute tests with appropriate Windows permissions +> (e.g. as Admin) if adding tests with "local machine" store context specified. \ No newline at end of file diff --git a/test/configs/certs/tlsauth/certstore/server.p12 b/test/configs/certs/tlsauth/certstore/server.p12 new file mode 100644 index 0000000000000000000000000000000000000000..9325afbc15cd755c85b08a3307aa55d4b6547e85 GIT binary patch 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