Author: Paolo Lulli <paolo@lulli.net>
Get certificates from web PKI
client/config/cli-config.go | 4 + client/csr.pem | 11 +++++ client/files.go | 24 +++++++++++ client/io.go | 63 +++++++++++++++++++++++++++++ client/main.go | 84 +++++++++++++-------------------------- client/pki.go | 40 ++++++----------- client/pkiclient.go | 56 ++++++++++++++++++++++++++ client/time-util.go | 38 +++++++++++++++++
diff --git a/client/config/cli-config.go b/client/config/cli-config.go index 6cd357b202469fdf2996f56b133c14ea1d1f1c4e..ae8bce2a047d0a14e2747ac78495abd9b2ee0277 100644 --- a/client/config/cli-config.go +++ b/client/config/cli-config.go @@ -22,9 +22,11 @@ TlsCertificate string `json:"tlsCertificate"` TlsVerifyServer string `json:"tlsVerifyServer"` // For certificate request over PKI endpoint - ClientCnName string `json:"clientCnName"` + ClientCertDir string `json:"clientCertDir"` + ClientCn string `json:"clientCn"` ClientOrganization string `json:"clientOrganization"` ClientEmail string `json:"clientEmail"` + PkiEndpoint string `json:"pkiEndpoint"` } func GetClientConfig(fileName string) ClientConfiguration { diff --git a/client/csr.pem b/client/csr.pem new file mode 100644 index 0000000000000000000000000000000000000000..50841fccb86c76b448844ae8964008b373a87c7f --- /dev/null +++ b/client/csr.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBjjCB+AIBADBPMRgwFgYDVQQKEw95YXRzLXNlcnZpY2UtY2ExDzANBgNVBAMT +BmNubmFtZTEiMCAGCSqGSIb3DQEJARYTZXhwZXJpbWVudEBlbWFpbC5jbzCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwCKr4yTflNFJw+9Lcb0q7JbXON6n7stj +XptCMt8h2nY4N/nSc74Tm0EfLwIDQovohyUMWJ+wI7dFwlt5/ZMWxqh+lzXXj2Nv +yLiJ9MhUqj2IONY4jeX2R97+uDzZiJhF2ri68NaYLG3tpQ3DCgnyFXXtj4qhSiCi +YsE+22WdGxMCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4GBAIwWVCtQO6udDwk/zT/6 +TDwhLBBY16rEiEZ3UNer3lont9MZOkDOQcKVdOWNIHl8aIvAYyiQYhHUfVOe6KmN +1EfTUykQMvL6FOR20Ye7Wbn3EX12b6lMGlyi/jLa7x6JmR5cM71Za/7udg522w3v +dh0XRe2eTB5ZyoI/Sd6N9DsS +-----END CERTIFICATE REQUEST----- diff --git a/client/files.go b/client/files.go new file mode 100644 index 0000000000000000000000000000000000000000..f6d9b0bd3bc397daa643e72c1be0c53a3c35dcfb --- /dev/null +++ b/client/files.go @@ -0,0 +1,24 @@ +/** + * Yats - yats + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Paolo Lulli <kevwe.com> + * @copyright Paolo Lulli 2024 + */ +package main + +import ( + "fmt" + "os" +) + +func makeDirIfItDoesNotExist(path string) { + if _, err := os.Stat(path); os.IsNotExist(err) { + err := os.MkdirAll(path, 0700) + if err != nil { + fmt.Printf("Error creating dir: %s %v", path, err) + } + } +} diff --git a/client/io.go b/client/io.go new file mode 100644 index 0000000000000000000000000000000000000000..450f0865ff95312f52a30271fde31a658c717fa1 --- /dev/null +++ b/client/io.go @@ -0,0 +1,63 @@ +/** + * Yats - yats + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Paolo Lulli <kevwe.com> + * @copyright Paolo Lulli 2024 + */ +package main + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "os" +) + +func mapFromBase64(value string) (map[string]any, bool) { + decodedBytes, err := base64.StdEncoding.DecodeString(value) + if err != nil { + fmt.Println("Error decoding string:", err) + return nil, true + } + + decoded := string(decodedBytes) + var dataMap map[string]any + json.Unmarshal([]byte(decoded), &dataMap) + return dataMap, false +} + +func getDataJson(list string) ([]byte, bool) { + var result map[string]interface{} + json.Unmarshal([]byte(list), &result) + innerData := result["data"] + + dataJson, er := json.Marshal(innerData) + if er != nil { + fmt.Printf("Parsing response: %v", er) + return nil, true + } + return dataJson, false +} + +func readSecretPin(prompt string) string { + var input string + fmt.Printf("%s\n", prompt) + _, err := fmt.Scan(&input) + if err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + return input +} + +func fileExists(fileName string) bool { + _, error := os.Stat(fileName) + + if os.IsNotExist(error) { + return false + } + return true +} diff --git a/client/main.go b/client/main.go index 436186c55859746dc52407008aef752362cec370..428119a29182ea1e90240540d824915c221e2fe5 100644 --- a/client/main.go +++ b/client/main.go @@ -14,8 +14,8 @@ "encoding/base64" "encoding/json" "fmt" "os" + "path/filepath" "sort" - "time" "yats/config" flag "github.com/spf13/pflag" @@ -102,7 +102,31 @@ os.Exit(0) } if *generatePki { - yatsClient.CreateCsr() + makeDirIfItDoesNotExist(cfg.ClientCertDir) + secretPin := readSecretPin("Insert PIN from email") + + csrFile := filepath.Join(cfg.ClientCertDir, cfg.ClientCn+".csr") + csr, err := yatsClient.CreateCsr(csrFile) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + base64EncodedCsr := base64.StdEncoding.EncodeToString(csr) + + certRequest := CertificateRequest{ + Csr: base64EncodedCsr, + SecretPin: secretPin, + ClientCn: cfg.ClientCn, + } + + jsonResponse := yatsClient.PkiPostCsr(yatsClient.config.PkiEndpoint, certRequest) + var certificateResponse CertificateResponse + json.Unmarshal(jsonResponse, &certificateResponse) + certPem, err := base64.StdEncoding.DecodeString(certificateResponse.Base64Certificate) + certificateFile := filepath.Join(cfg.ClientCertDir, cfg.ClientCn+".crt") + os.WriteFile(certificateFile, certPem, 0644) + fmt.Printf("Certificate written on: [%s]", certificateFile) os.Exit(0) } @@ -227,7 +251,7 @@ err := json.Unmarshal(dataJson, &metricListModel) if err == nil { metric := metricListModel[0] - dataMap, errNoValue := loadBase64ValueAsMap(metric) + dataMap, errNoValue := mapFromBase64(metric.Value) if errNoValue { return } @@ -243,7 +267,7 @@ fmt.Printf("\"%s\"%s", k, recordSeparator) } fmt.Printf("%s", rowSeparator) for _, row := range metricListModel { - rowMap, errNoValue := loadBase64ValueAsMap(row) + rowMap, errNoValue := mapFromBase64(row.Value) if errNoValue { return } @@ -275,55 +299,3 @@ fmt.Printf("%s", listResponse) os.Exit(0) } } - -func formattedUtcTimeInSec(int64Ts int64) string { - unixTimeUTC := time.Unix(int64Ts/1000, 0) - return unixTimeUTC.Format(time.RFC3339) -} - -func loadBase64ValueAsMap(metric MetricModel) (map[string]any, bool) { - decodedBytes, err := base64.StdEncoding.DecodeString(metric.Value) - if err != nil { - fmt.Println("Error decoding string:", err) - return nil, true - } - - decoded := string(decodedBytes) - var dataMap map[string]any - json.Unmarshal([]byte(decoded), &dataMap) - return dataMap, false -} - -func getDataJson(list string) ([]byte, bool) { - var result map[string]interface{} - json.Unmarshal([]byte(list), &result) - innerData := result["data"] - - dataJson, er := json.Marshal(innerData) - if er != nil { - fmt.Printf("Parsing response: %v", er) - return nil, true - } - return dataJson, false -} - -func calculateCutDate(ts int64, sinceSeconds *bool, sinceMinutes *bool, sinceHours *bool, sinceDays *bool, sinceYears *bool) int64 { - if 0 != ts { - if *sinceSeconds { - return time.Now().Unix() - ts - } - if *sinceMinutes { - return time.Now().Unix() - ts*60 - } - if *sinceHours { - return time.Now().Unix() - ts*60*60 - } - if *sinceDays { - return time.Now().Unix() - ts*60*60*24 - } - if *sinceYears { - return time.Now().Unix() - ts*60*60*24*365 - } - } - return ts -} diff --git a/client/pki.go b/client/pki.go index a0b1415e771b4c1afcba3b6227b2abe3c73a214f..184f43d0f696f28a793259af70d13c2ab8307485 100644 --- a/client/pki.go +++ b/client/pki.go @@ -5,23 +5,13 @@ "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" - "encoding/asn1" "encoding/pem" "fmt" "os" ) -func fileExists(fileName string) bool { - _, error := os.Stat(fileName) - - if os.IsNotExist(error) { - return false - } - return true -} - -func (c *YatsClient) CreateCsr() ([]byte, error) { - var oidEmailAddress = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1} +func (c *YatsClient) CreateCsr(csrFile string) ([]byte, error) { + //var oidEmailAddress = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1} if fileExists(c.config.TlsKeyFile) { fmt.Printf("Private key file already exists: %v\n", c.config.TlsKeyFile) @@ -33,8 +23,8 @@ fmt.Printf("Certificate file already exists: %v\n", c.config.TlsKeyFile) os.Exit(-1) } - if ("" == c.config.ClientCnName) || ("" == c.config.ClientOrganization) || ("" == c.config.ClientCnName) { - fmt.Printf("Client name: [%v], organization: [%v], email address: [%v]\n", c.config.ClientCnName, c.config.ClientOrganization, c.config.ClientCnName) + if ("" == c.config.ClientCn) || ("" == c.config.ClientOrganization) || ("" == c.config.ClientEmail) { + fmt.Printf("Client name: [%v], organization: [%v], email address: [%v]\n", c.config.ClientCn, c.config.ClientOrganization, c.config.ClientEmail) fmt.Println("No param can be empty, exiting") os.Exit(-1) } @@ -50,7 +40,7 @@ os.WriteFile(c.config.TlsKeyFile, pemdata, 0644) subj := pkix.Name{ - CommonName: c.config.ClientCnName, + CommonName: c.config.ClientCn, /* Country: []string{"AU"}, Province: []string{"Some-State"}, @@ -58,16 +48,17 @@ Locality: []string{"MyCity"}, */ Organization: []string{c.config.ClientOrganization}, //OrganizationalUnit: []string{organizationalUnit}, - - ExtraNames: []pkix.AttributeTypeAndValue{ - { - Type: oidEmailAddress, - Value: asn1.RawValue{ - Tag: asn1.TagIA5String, - Bytes: []byte(c.config.ClientCnName), + /* + ExtraNames: []pkix.AttributeTypeAndValue{ + { + Type: oidEmailAddress, + Value: asn1.RawValue{ + Tag: asn1.TagIA5String, + Bytes: []byte(c.config.ClientEmail), + }, }, }, - }, + */ } template := x509.CertificateRequest{ @@ -76,7 +67,6 @@ SignatureAlgorithm: x509.SHA256WithRSA, } csrBytes, _ := x509.CreateCertificateRequest(rand.Reader, &template, keyBytes) - pem.Encode(os.Stdout, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}) pemcsrdata := pem.EncodeToMemory( &pem.Block{ @@ -85,6 +75,6 @@ Bytes: csrBytes, }, ) - os.WriteFile("csr.pem", pemcsrdata, 0644) + os.WriteFile(csrFile, pemcsrdata, 0644) return pemcsrdata, nil } diff --git a/client/pkiclient.go b/client/pkiclient.go new file mode 100644 index 0000000000000000000000000000000000000000..890a5044d2a50a6430778381f6296a8706a59a3b --- /dev/null +++ b/client/pkiclient.go @@ -0,0 +1,56 @@ +/** + * Yats - yats + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Paolo Lulli <kevwe.com> + * @copyright Paolo Lulli 2024 + */ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" +) + +func (c *YatsClient) PkiPostCsr(endpoint string, certRequest CertificateRequest) []byte { + client := http.Client{ + Timeout: 30 * time.Second, + } + + body, err := json.Marshal(certRequest) + + //fmt.Printf("body: -> %s\n", body) + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json; charset=UTF-8") + if err != nil { + fmt.Println("Unable to make POST request", err) + os.Exit(1) + } + req.Header.Add("Accept", "*/*") + resp, err := client.Do(req) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer resp.Body.Close() + data, _ := io.ReadAll(resp.Body) + + return data +} + +type CertificateRequest struct { + ClientCn string `json:"clientCn"` + Csr string `json:"csr"` + SecretPin string `json:"pin"` +} + +type CertificateResponse struct { + Base64Certificate string `json:"base64cert"` +} diff --git a/client/time-util.go b/client/time-util.go new file mode 100644 index 0000000000000000000000000000000000000000..3d4b2390fa1bf43c6f9b1261e734808caf4c9d7f --- /dev/null +++ b/client/time-util.go @@ -0,0 +1,38 @@ +/** + * Yats - yats + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Paolo Lulli <kevwe.com> + * @copyright Paolo Lulli 2024 + */ +package main + +import "time" + +func formattedUtcTimeInSec(int64Ts int64) string { + unixTimeUTC := time.UnixMilli(int64Ts) + return unixTimeUTC.Format(time.RFC3339) +} + +func calculateCutDate(ts int64, sinceSeconds *bool, sinceMinutes *bool, sinceHours *bool, sinceDays *bool, sinceYears *bool) int64 { + if 0 != ts { + if *sinceSeconds { + return time.Now().Unix() - ts + } + if *sinceMinutes { + return time.Now().Unix() - ts*60 + } + if *sinceHours { + return time.Now().Unix() - ts*60*60 + } + if *sinceDays { + return time.Now().Unix() - ts*60*60*24 + } + if *sinceYears { + return time.Now().Unix() - ts*60*60*24*365 + } + } + return ts +}