delete create_certs functionality
This commit is contained in:
parent
3a7ce8c6e2
commit
bbb2700a75
12 changed files with 6 additions and 480 deletions
|
|
@ -30,7 +30,6 @@ RUN pip3 install -r requirements.txt
|
|||
WORKDIR /bumper
|
||||
|
||||
# Copy only required folders instead of all
|
||||
COPY create_certs/ create_certs/
|
||||
COPY bumper/ bumper/
|
||||
|
||||
ENTRYPOINT ["python3", "-m", "bumper"]
|
||||
|
|
|
|||
|
|
@ -189,54 +189,6 @@ async def shutdown():
|
|||
bumperlog.info("Shutdown complete")
|
||||
|
||||
|
||||
def create_certs():
|
||||
import platform
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
path = os.path.dirname(sys.modules[__name__].__file__)
|
||||
path = os.path.join(path, "..")
|
||||
sys.path.insert(0, path)
|
||||
|
||||
print("Creating certificates")
|
||||
odir = os.path.realpath(os.curdir)
|
||||
os.chdir("certs")
|
||||
if str(platform.system()).lower() == "windows":
|
||||
# run for win
|
||||
subprocess.run([os.path.join("..", "create_certs", "create_certs_windows.exe")])
|
||||
elif str(platform.system()).lower() == "darwin":
|
||||
# run on mac
|
||||
subprocess.run([os.path.join("..", "create_certs", "create_certs_osx")])
|
||||
elif str(platform.system()).lower() == "linux":
|
||||
if "arm" in platform.machine().lower() or "aarch64" in platform.machine().lower():
|
||||
# run for pi
|
||||
subprocess.run([os.path.join("..", "create_certs", "create_certs_rpi")])
|
||||
else:
|
||||
# run for linux
|
||||
subprocess.run([os.path.join("..", "create_certs", "create_certs_linux")])
|
||||
|
||||
else:
|
||||
os.chdir(odir)
|
||||
bumperlog.fatal("Can't determine platform. Create certs manually and try again.")
|
||||
return
|
||||
|
||||
print("Certificates created")
|
||||
os.chdir(odir)
|
||||
|
||||
if "__main__.py" in sys.argv[0]:
|
||||
os.execv(
|
||||
sys.executable, ["python", "-m", "bumper"] + sys.argv[1:]
|
||||
) # Start again
|
||||
|
||||
else:
|
||||
os.execv(sys.executable, ["python"] + sys.argv) # Start again
|
||||
|
||||
|
||||
def first_run():
|
||||
create_certs()
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
import argparse
|
||||
|
||||
|
|
@ -252,8 +204,9 @@ def main(argv=None):
|
|||
and os.path.exists(server_cert)
|
||||
and os.path.exists(server_key)
|
||||
):
|
||||
first_run()
|
||||
return
|
||||
msg = "No certs found! Please generate them (More infos in the docs)"
|
||||
bumperlog.fatal(msg)
|
||||
sys.exit(msg)
|
||||
|
||||
if not (
|
||||
os.path.exists(os.path.join(data_dir, "passwd"))
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
ecovacs.com
|
||||
*.ecovacs.com
|
||||
ecouser.net
|
||||
*.ecouser.net
|
||||
ecovacs.net
|
||||
*.ecovacs.net
|
||||
*.ww.ecouser.net
|
||||
*.dc-eu.ww.ecouser.net
|
||||
*.dc.ww.ecouser.net
|
||||
*.area.ww.ecouser.net
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,12 +0,0 @@
|
|||
#!/bin/bash
|
||||
SCRIPTPATH=$(dirname "$BASH_SOURCE")
|
||||
OUTPUTPATH=$(dirname $(dirname "$BASH_SOURCE"))
|
||||
echo "Building for Windows: $OUTPUTPATH/create_certs_windows.exe"
|
||||
GOOS=windows go build -o $OUTPUTPATH/create_certs_windows.exe $SCRIPTPATH/create_certs.go
|
||||
echo "Building for OSX $OUTPUTPATH/create_certs_osx"
|
||||
GOOS=darwin go build -o $OUTPUTPATH/create_certs_osx $SCRIPTPATH/create_certs.go
|
||||
echo "Building for Linux $OUTPUTPATH/create_certs_linux"
|
||||
GOOS=linux go build -o $OUTPUTPATH/create_certs_linux $SCRIPTPATH/create_certs.go
|
||||
echo "Building for ARM (Raspberry Pi) $OUTPUTPATH/create_certs_rpi"
|
||||
GOOS=linux GOARCH=arm GOARM=5 go build -o $OUTPUTPATH/create_certs_rpi $SCRIPTPATH/create_certs.go
|
||||
echo "Build Complete"
|
||||
|
|
@ -1,262 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
var InBumperSan string
|
||||
var OutCertDirectory string
|
||||
|
||||
func setFlags() {
|
||||
|
||||
exePath, _ := os.Executable()
|
||||
currentDir, _ := os.Getwd()
|
||||
|
||||
//certPath will be current working directory by defaultß
|
||||
certPath, err := filepath.Abs(currentDir)
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
}
|
||||
|
||||
//sanPath will be exe path /Bumper_SAN.txt by default
|
||||
sanPath, err := filepath.Abs(filepath.Join(exePath, "..", "/Bumper_SAN.txt"))
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
}
|
||||
|
||||
flag.StringVar(&InBumperSan, "inSAN", sanPath, "Input file containing a list of Subject Alternate Names (line separated)")
|
||||
flag.StringVar(&OutCertDirectory, "out", certPath, "Directory to output certificates to")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
}
|
||||
func main() {
|
||||
|
||||
setFlags()
|
||||
|
||||
fmt.Printf("-------- Create_Certs --------\n")
|
||||
|
||||
//get absolute path
|
||||
outCertDirectory, _ := filepath.Abs(OutCertDirectory)
|
||||
dexists, isdfile := pathExistsType(outCertDirectory)
|
||||
if !dexists {
|
||||
log.Fatalf("Certs directory doesn't exist: %v", outCertDirectory)
|
||||
}
|
||||
if isdfile {
|
||||
log.Fatalf("Certs directory is a file, not a directory: %v", outCertDirectory)
|
||||
}
|
||||
|
||||
//get absolute path
|
||||
inBumperSan, _ := filepath.Abs(InBumperSan)
|
||||
bexists, isbfile := pathExistsType(inBumperSan)
|
||||
if !bexists {
|
||||
log.Printf("Bumper SAN doesn't exist, certificate won't contain Subject Alternate Names: %v\n", inBumperSan)
|
||||
inBumperSan = ""
|
||||
}
|
||||
if bexists && !isbfile {
|
||||
log.Printf("Bumper SAN is a directory instead of file, certificate won't contain Subject Alternate Names: %v\n", inBumperSan)
|
||||
inBumperSan = ""
|
||||
}
|
||||
|
||||
fmt.Printf("-------- Starting Certificate Creation --------\n")
|
||||
fmt.Printf("Options: \n Output Directory: %v\n Input SAN List: %v\n", outCertDirectory, inBumperSan)
|
||||
|
||||
make_CA(outCertDirectory)
|
||||
signCert(outCertDirectory, inBumperSan)
|
||||
|
||||
fmt.Printf("-------- Certificate Creation Complete --------\n")
|
||||
}
|
||||
|
||||
func make_CA(outCertDirectory string) {
|
||||
|
||||
fmt.Printf("-------- Creating CA Cert --------\n")
|
||||
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
pub := &priv.PublicKey
|
||||
|
||||
ca := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1653),
|
||||
Subject: pkix.Name{
|
||||
CommonName: string("Bumper CA"),
|
||||
Organization: []string{"Bumper"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(2, 0, 0),
|
||||
SubjectKeyId: bigIntHash(priv.N),
|
||||
AuthorityKeyId: bigIntHash(priv.N),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{},
|
||||
IsCA: true,
|
||||
BasicConstraintsValid: true,
|
||||
MaxPathLen: -1,
|
||||
}
|
||||
|
||||
ca_b, err := x509.CreateCertificate(rand.Reader, ca, ca, pub, priv)
|
||||
if err != nil {
|
||||
log.Fatalf("Create ca failed: %v", err)
|
||||
}
|
||||
|
||||
// Public key
|
||||
certOut, err := os.Create(filepath.Join(outCertDirectory, "ca.crt"))
|
||||
if err != nil {
|
||||
log.Fatalf("Create ca.crt failed: %v", err)
|
||||
}
|
||||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: ca_b})
|
||||
certOut.Close()
|
||||
log.Printf("ca.crt created at %v\n", filepath.Join(outCertDirectory, "ca.crt"))
|
||||
|
||||
// Private key
|
||||
keyOut, err := os.OpenFile(filepath.Join(outCertDirectory, "ca.key"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
log.Fatalf("Create ca.key failed: %v", err)
|
||||
}
|
||||
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
keyOut.Close()
|
||||
log.Printf("ca.key created at %v\n", filepath.Join(outCertDirectory, "ca.key"))
|
||||
}
|
||||
|
||||
func signCert(outCertDirectory string, inBumperSan string) {
|
||||
|
||||
fmt.Printf("-------- Creating Server Cert --------\n")
|
||||
// Load CA
|
||||
catls, err := tls.LoadX509KeyPair(filepath.Join(outCertDirectory, "ca.crt"), filepath.Join(outCertDirectory, "ca.key"))
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading ca cert: %v", err)
|
||||
}
|
||||
|
||||
ca, err := x509.ParseCertificate(catls.Certificate[0])
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing ca cert: %v", err)
|
||||
}
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
ifaces, err := net.Interfaces()
|
||||
var ips []net.IP
|
||||
// handle err
|
||||
for _, i := range ifaces {
|
||||
addrs, _ := i.Addrs()
|
||||
// handle err
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
// process IP address
|
||||
}
|
||||
}
|
||||
ips = append(ips, net.ParseIP("127.0.0.1"))
|
||||
|
||||
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
pubKey := &privateKey.PublicKey
|
||||
|
||||
//DNS/SAN names for cert
|
||||
dnsNames := []string{hostname, "localhost"}
|
||||
//Read SANs from file
|
||||
//get absolute path
|
||||
|
||||
bexists, isbfile := pathExistsType(inBumperSan)
|
||||
if !bexists {
|
||||
log.Print("Bumper SAN doesn't exist, certificate won't contain Subject Alternate Names")
|
||||
}
|
||||
if bexists && !isbfile {
|
||||
log.Print("Bumper SAN is a directory instead of file, certificate won't contain Subject Alternate Names")
|
||||
}
|
||||
if bexists && isbfile {
|
||||
sans, err := readLines(inBumperSan)
|
||||
if err != nil {
|
||||
log.Printf("Error reading %v certificates will be created without Subject Alternate Names: %v", inBumperSan, err)
|
||||
}
|
||||
dnsNames = append(dnsNames, sans...)
|
||||
}
|
||||
|
||||
// Prepare certificate
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1658),
|
||||
Issuer: ca.Subject,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "Bumper Server",
|
||||
Organization: []string{"Bumper"},
|
||||
},
|
||||
SubjectKeyId: bigIntHash(privateKey.N),
|
||||
AuthorityKeyId: ca.RawSubject,
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(2, 0, 0),
|
||||
IsCA: false,
|
||||
BasicConstraintsValid: true,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
DNSNames: dnsNames,
|
||||
IPAddresses: ips,
|
||||
}
|
||||
|
||||
// Sign the certificate
|
||||
cert_b, err := x509.CreateCertificate(rand.Reader, &template, ca, pubKey, catls.PrivateKey)
|
||||
|
||||
// Public key
|
||||
certOut, err := os.Create(filepath.Join(outCertDirectory, "bumper.crt"))
|
||||
if err != nil {
|
||||
log.Fatalf("Create bumper.crt failed: %v", err)
|
||||
}
|
||||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert_b})
|
||||
certOut.Close()
|
||||
log.Printf("bumper.crt created at %v\n", filepath.Join(outCertDirectory, "bumper.crt"))
|
||||
|
||||
// Private key
|
||||
keyOut, err := os.OpenFile(filepath.Join(outCertDirectory, "bumper.key"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
log.Fatalf("create bumper.key failed: %v", err)
|
||||
}
|
||||
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
|
||||
keyOut.Close()
|
||||
log.Printf("bumper.key created at %v\n", filepath.Join(outCertDirectory, "bumper.key"))
|
||||
}
|
||||
|
||||
func bigIntHash(n *big.Int) []byte {
|
||||
h := sha1.New()
|
||||
h.Write(n.Bytes())
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// readLines reads a whole file into memory
|
||||
// and returns a slice of its lines.
|
||||
func readLines(path string) ([]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
return lines, scanner.Err()
|
||||
}
|
||||
|
||||
// pathexistsType checks if a path exists and is a file or directory
|
||||
func pathExistsType(filename string) (exists bool, isfile bool) {
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false, false
|
||||
}
|
||||
return true, !info.IsDir()
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestArgs(t *testing.T) {
|
||||
|
||||
orArgs := os.Args
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet(orArgs[0], flag.ContinueOnError)
|
||||
os.Args = []string{"cmd", "-inSAN", "123"}
|
||||
setFlags()
|
||||
|
||||
if InBumperSan != "123" {
|
||||
t.Error("InBumperSan not set by arg")
|
||||
}
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet(orArgs[0], flag.ContinueOnError)
|
||||
os.Args = []string{"cmd", "-out", "456"}
|
||||
setFlags()
|
||||
|
||||
if OutCertDirectory != "456" {
|
||||
t.Error("Out path not set by arg")
|
||||
}
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet(orArgs[0], flag.ContinueOnError)
|
||||
os.Args = []string{"cmd", "-inSAN", "san1", "-out", "out2"}
|
||||
setFlags()
|
||||
|
||||
if InBumperSan != "san1" {
|
||||
t.Error("InBumperSan not set by arg")
|
||||
}
|
||||
if OutCertDirectory != "out2" {
|
||||
t.Error("Out path not set by arg")
|
||||
}
|
||||
|
||||
os.Args = orArgs
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ Users can generate certificates in the following ways:
|
|||
|
||||
| Method | Description |
|
||||
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Create_Certs](#creating-certs-using-createcerts) | ***(Preferred)*** This is a utility that has been created to assist in generating the certificates required easily. |
|
||||
| [OpenSSL](#manually-create-certs-with-openssl) | Users can manually create the same certificates as Create_Certs by utilizing OpenSSL. |
|
||||
| [Custom CA/Self](#using-a-custom-caself) | If a user has their own CA the certificates can be generated there and used within Bumper. |
|
||||
|
||||
|
|
@ -17,39 +16,9 @@ Users can generate certificates in the following ways:
|
|||
* A CA Cert must be provided that can be imported into devices (phones, browsers, etc).
|
||||
* Server certificate should include [SANs (Subject Alternate Names)](#subject-alternative-name) for all of the *.ecovacs, etc domains.
|
||||
|
||||
## Creating certs using Create_Certs
|
||||
|
||||
Create_Certs was created to ease creation of certificates specifically for Bumper. Binaries are provided for Windows/Linux/OSX/RPi (ARMv5) in the Create_Certs directory.
|
||||
|
||||
| Binary | Platform |
|
||||
| ------------------------ | -------------------- |
|
||||
| create_certs_linux | Linux |
|
||||
| create_certs_osx | macOS/x |
|
||||
| create_certs_windows.exe | Windows |
|
||||
| create_certs_rpi | RaspberryPi (ARM v5) |
|
||||
|
||||
Create_Certs is written in Go which allows cross-platform compiling. If the binaries don't work on your platform, install Go.
|
||||
|
||||
With Go installed you can:
|
||||
|
||||
* Execute the go code - `go run create_certs/src/create_certs.go`
|
||||
* Build a new binary for your platform - `go build create_certs/src/create_certs.go`
|
||||
|
||||
### Usage
|
||||
|
||||
Create_Certs will automatically create the required certificates in the directory it is executed from. For best results change to the {bumper_home}/certs directory prior to executing, otherwise you'll need to move the certs after.
|
||||
|
||||
1. `cd certs`
|
||||
2. Execute create_certs (using the binary fitting your platform) - `../create_certs/create_certs_{platform}`
|
||||
3. The certificates are generated and should be available in the current directory (certs)
|
||||
|
||||
## Subject Alternative Name
|
||||
|
||||
The server certificate requires a number of SAN (Subject Alternative Names) be added. Create_Certs handles this automatically by loading any SANs listed in the `create_certs/Bumper_SAN.txt` file. If creating certificates manually via OpenSSl/Custom CA these will need to be added.
|
||||
|
||||
## Manually create certs with OpenSSL
|
||||
|
||||
I get it, you don't trust create_certs and want to do it manually. The easiest way to create the required certs is at https://certificatetools.com/. In fact the below OpenSSL commands come straight from that site, post creation via the GUI.
|
||||
The easiest way to create the required certs is at https://certificatetools.com/. In fact the below OpenSSL commands come straight from that site, post creation via the GUI.
|
||||
|
||||
### Create a Root CA
|
||||
|
||||
|
|
@ -179,4 +148,4 @@ DNS.9 = *.area.ww.ecouser.net
|
|||
|
||||
## Using a Custom CA/Self
|
||||
|
||||
This should work siimilar to the OpenSSL method. Ensure the server certificate has the proper [SANs](#subject-alternative-name) in place.
|
||||
This should work siimilar to the OpenSSL method. Ensure the server certificate has the proper SANs (see above) in place.
|
||||
|
|
@ -1,16 +1,6 @@
|
|||
import mock
|
||||
from mock import patch
|
||||
import pytest
|
||||
from tinydb.storages import MemoryStorage
|
||||
from tinydb import TinyDB, Query
|
||||
|
||||
import bumper
|
||||
import os
|
||||
import datetime, time
|
||||
import platform
|
||||
import json
|
||||
import asyncio
|
||||
from testfixtures import LogCapture
|
||||
import sys
|
||||
|
||||
|
||||
def mock_subrun(*args):
|
||||
|
|
@ -40,63 +30,3 @@ def test_argparse(mock_start):
|
|||
assert bumper.bumper_announce_ip == "127.0.0.1"
|
||||
assert bumper.bumper_listen == "127.0.0.1"
|
||||
assert mock_start.called == True
|
||||
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch("platform.system")
|
||||
@patch("platform.machine")
|
||||
@patch("os.execv")
|
||||
def test_createcert(mock_run, mock_platform, mock_machine, mock_exec):
|
||||
mock_run.side_effect = mock_subrun
|
||||
platform.system.return_value = "darwin"
|
||||
bumper.create_certs()
|
||||
assert mock_run.called == True
|
||||
assert (
|
||||
os.path.join("..", "create_certs", "create_certs_osx")
|
||||
in mock_exec.call_args.args[0]
|
||||
)
|
||||
|
||||
platform.system.return_value = "windows"
|
||||
bumper.create_certs()
|
||||
assert mock_run.called == True
|
||||
assert (
|
||||
os.path.join("..", "create_certs", "create_certs_windows.exe")
|
||||
in mock_exec.call_args.args[0]
|
||||
)
|
||||
|
||||
platform.system.return_value = "linux"
|
||||
bumper.create_certs()
|
||||
assert mock_run.called == True
|
||||
assert (
|
||||
os.path.join("..", "create_certs", "create_certs_linux")
|
||||
in mock_exec.call_args.args[0]
|
||||
)
|
||||
|
||||
platform.system.return_value = "linux"
|
||||
platform.machine.return_value = "arm"
|
||||
bumper.create_certs()
|
||||
assert mock_run.called == True
|
||||
assert (
|
||||
os.path.join("..", "create_certs", "create_certs_rpi")
|
||||
in mock_exec.call_args.args[0]
|
||||
)
|
||||
|
||||
with LogCapture() as l:
|
||||
platform.system.return_value = "nixbad"
|
||||
bumper.create_certs()
|
||||
|
||||
l.check_present(
|
||||
(
|
||||
"root",
|
||||
"CRITICAL",
|
||||
"Can't determine platform. Create certs manually and try again.",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@patch("bumper.first_run")
|
||||
def test_main(mock_firstrun):
|
||||
bumper.ca_cert = "sf"
|
||||
bumper.main()
|
||||
assert mock_firstrun.called == True
|
||||
bumper.ca_cert = "tests/test_certs/ca.crt"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue