mirror of
https://github.com/1f349/orchid.git
synced 2024-12-22 08:04:10 +00:00
Use yaml and fix up setup code
This commit is contained in:
parent
63472f32bf
commit
43905a3dc3
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
*.sqlite
|
*.sqlite
|
||||||
*.local
|
*.local
|
||||||
|
.data/
|
||||||
|
@ -3,19 +3,14 @@ package main
|
|||||||
import "github.com/MrMelon54/orchid/renewal"
|
import "github.com/MrMelon54/orchid/renewal"
|
||||||
|
|
||||||
type startUpConfig struct {
|
type startUpConfig struct {
|
||||||
Database string `json:"db"`
|
Listen string `yaml:"listen"`
|
||||||
PrivKey string `json:"priv_key"`
|
Acme acmeConfig `yaml:"acme"`
|
||||||
PubKey string `json:"pub_key"`
|
LE renewal.LetsEncryptConfig `yaml:"letsEncrypt"`
|
||||||
Listen string `json:"listen"`
|
Domains []string `yaml:"domains"`
|
||||||
Acme acmeConfig `json:"acme"`
|
|
||||||
LE renewal.LetsEncryptConfig `json:"lets_encrypt"`
|
|
||||||
Domains []string `json:"domains"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type acmeConfig struct {
|
type acmeConfig struct {
|
||||||
Access string `json:"access"`
|
PresentUrl string `yaml:"presentUrl"`
|
||||||
Refresh string `json:"refresh"`
|
CleanUpUrl string `yaml:"cleanUpUrl"`
|
||||||
PresentUrl string `json:"present_url"`
|
RefreshUrl string `yaml:"refreshUrl"`
|
||||||
CleanUpUrl string `json:"clean_up_url"`
|
|
||||||
RefreshUrl string `json:"refresh_url"`
|
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/MrMelon54/mjwt"
|
"github.com/MrMelon54/mjwt"
|
||||||
@ -12,6 +11,8 @@ import (
|
|||||||
"github.com/MrMelon54/orchid/servers"
|
"github.com/MrMelon54/orchid/servers"
|
||||||
"github.com/MrMelon54/violet/utils"
|
"github.com/MrMelon54/violet/utils"
|
||||||
"github.com/google/subcommands"
|
"github.com/google/subcommands"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@ -53,7 +54,7 @@ func (s *serveCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
|
|||||||
}
|
}
|
||||||
|
|
||||||
var conf startUpConfig
|
var conf startUpConfig
|
||||||
err = json.NewDecoder(openConf).Decode(&conf)
|
err = yaml.NewDecoder(openConf).Decode(&conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("[Orchid] Error: invalid config file: ", err)
|
log.Println("[Orchid] Error: invalid config file: ", err)
|
||||||
return subcommands.ExitFailure
|
return subcommands.ExitFailure
|
||||||
@ -74,14 +75,14 @@ func normalLoad(conf startUpConfig, wd string) {
|
|||||||
// open sqlite database
|
// open sqlite database
|
||||||
db, err := sql.Open("sqlite3", filepath.Join(wd, "orchid.db.sqlite"))
|
db, err := sql.Open("sqlite3", filepath.Join(wd, "orchid.db.sqlite"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("[Orchid] Failed to open database")
|
log.Fatal("[Orchid] Failed to open database:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
certDir := filepath.Join(wd, "certs")
|
certDir := filepath.Join(wd, "certs")
|
||||||
keyDir := filepath.Join(wd, "keys")
|
keyDir := filepath.Join(wd, "keys")
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
acmeProv := httpAcme.NewHttpAcmeProvider(conf.Acme.Access, conf.Acme.Refresh, conf.Acme.PresentUrl, conf.Acme.CleanUpUrl, conf.Acme.RefreshUrl)
|
acmeProv, _ := httpAcme.NewHttpAcmeProvider(filepath.Join(wd, "tokens.json"), conf.Acme.PresentUrl, conf.Acme.CleanUpUrl, conf.Acme.RefreshUrl)
|
||||||
renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir)
|
renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("[Orchid] Error:", err)
|
log.Fatal("[Orchid] Error:", err)
|
||||||
|
@ -1,12 +1,25 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
httpAcme "github.com/MrMelon54/orchid/http-acme"
|
||||||
|
"github.com/MrMelon54/orchid/renewal"
|
||||||
"github.com/google/subcommands"
|
"github.com/google/subcommands"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type setupCmd struct{ wdPath string }
|
type setupCmd struct{ wdPath string }
|
||||||
@ -44,10 +57,153 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
|
|||||||
|
|
||||||
var answers struct {
|
var answers struct {
|
||||||
ApiListen string
|
ApiListen string
|
||||||
FirstDomains []string
|
FirstDomains string
|
||||||
|
AcmeRefresh string
|
||||||
|
AcmePresentUrl string
|
||||||
|
AcmeCleanUpUrl string
|
||||||
|
AcmeRefreshUrl string
|
||||||
|
LEEmail string
|
||||||
}
|
}
|
||||||
_ = answers
|
_ = answers
|
||||||
|
|
||||||
// ask main questions
|
// ask main questions
|
||||||
return subcommands.ExitUsageError
|
err = survey.Ask([]*survey.Question{
|
||||||
|
{
|
||||||
|
Name: "ApiListen",
|
||||||
|
Prompt: &survey.Input{Message: "API listen address", Default: "127.0.0.1:8080"},
|
||||||
|
Validate: listenAddressValidator,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ApiDomains",
|
||||||
|
Prompt: &survey.Input{Message: "API Domains", Help: "Comma separated list of domains which can be edited by the API"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "LEEmail",
|
||||||
|
Prompt: &survey.Input{Message: "Lets Encrypt account email", Help: "Creates an account if it doesn't exist"},
|
||||||
|
Validate: survey.Required,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "AcmeRefresh",
|
||||||
|
Prompt: &survey.Input{Message: "ACME API Refresh Token"},
|
||||||
|
},
|
||||||
|
}, &answers)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("[Orchid] Error: ", err)
|
||||||
|
return subcommands.ExitFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
if answers.AcmeRefresh != "" {
|
||||||
|
err = survey.Ask([]*survey.Question{
|
||||||
|
{
|
||||||
|
Name: "AcmePresentUrl",
|
||||||
|
Prompt: &survey.Input{Message: "ACME API Present URL"},
|
||||||
|
Validate: urlValidator,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "AcmeCleanUpUrl",
|
||||||
|
Prompt: &survey.Input{Message: "ACME API Clean Up URL"},
|
||||||
|
Validate: urlValidator,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "AcmeRefreshUrl",
|
||||||
|
Prompt: &survey.Input{Message: "ACME API Refresh URL"},
|
||||||
|
Validate: urlValidator,
|
||||||
|
},
|
||||||
|
}, &answers)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("[Orchid] Error: ", err)
|
||||||
|
return subcommands.ExitFailure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := rsa.GenerateKey(rand.New(rand.NewSource(time.Now().UnixNano())), 4096)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("[Orchid] Error: ", err)
|
||||||
|
return subcommands.ExitFailure
|
||||||
|
}
|
||||||
|
keyBytes := x509.MarshalPKCS1PrivateKey(key)
|
||||||
|
keyBuf := new(bytes.Buffer)
|
||||||
|
err = pem.Encode(keyBuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("[Orchid] Error: ", err)
|
||||||
|
return subcommands.ExitFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
// write config file
|
||||||
|
confFile := filepath.Join(wdAbs, "config.yml")
|
||||||
|
createConf, err := os.Create(confFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("[Orchid] Failed to create config file: ", err)
|
||||||
|
return subcommands.ExitFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
confEncode := yaml.NewEncoder(createConf)
|
||||||
|
confEncode.SetIndent(2)
|
||||||
|
err = confEncode.Encode(startUpConfig{
|
||||||
|
Listen: answers.ApiListen,
|
||||||
|
Acme: acmeConfig{
|
||||||
|
PresentUrl: answers.AcmePresentUrl,
|
||||||
|
CleanUpUrl: answers.AcmeCleanUpUrl,
|
||||||
|
RefreshUrl: answers.AcmeRefreshUrl,
|
||||||
|
},
|
||||||
|
LE: renewal.LetsEncryptConfig{
|
||||||
|
Account: renewal.LetsEncryptAccount{
|
||||||
|
Email: answers.LEEmail,
|
||||||
|
PrivateKey: keyBuf.String(),
|
||||||
|
},
|
||||||
|
Directory: "production",
|
||||||
|
Certificate: "default",
|
||||||
|
},
|
||||||
|
Domains: strings.Split(answers.FirstDomains, ","),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("[Orchid] Failed to write config file: ", err)
|
||||||
|
return subcommands.ExitFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
// write token file
|
||||||
|
tokenFile := filepath.Join(wdAbs, "tokens.yml")
|
||||||
|
createTokens, err := os.Create(tokenFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("[Orchid] Failed to create tokens file: ", err)
|
||||||
|
return subcommands.ExitFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
confEncode = yaml.NewEncoder(createTokens)
|
||||||
|
confEncode.SetIndent(2)
|
||||||
|
err = confEncode.Encode(httpAcme.AcmeLogin{
|
||||||
|
Access: "",
|
||||||
|
Refresh: answers.AcmeRefresh,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("[Orchid] Failed to write tokens file: ", err)
|
||||||
|
return subcommands.ExitFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("[Orchid] Setup complete")
|
||||||
|
fmt.Printf("[Orchid] Run the renewal service with `orchid serve -conf %s`\n", confFile)
|
||||||
|
|
||||||
|
return subcommands.ExitSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
func listenAddressValidator(ans interface{}) error {
|
||||||
|
if ansStr, ok := ans.(string); ok {
|
||||||
|
// empty string means disable
|
||||||
|
if ansStr == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// use ResolveTCPAddr to validate the input
|
||||||
|
_, err := net.ResolveTCPAddr("tcp", ansStr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlValidator(ans interface{}) error {
|
||||||
|
if ansStr, ok := ans.(string); ok {
|
||||||
|
_, err := url.Parse(ansStr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/go-acme/lego/v4/challenge"
|
"github.com/go-acme/lego/v4/challenge"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -13,16 +16,34 @@ var _ challenge.Provider = &HttpAcmeProvider{}
|
|||||||
// HttpAcmeProvider sends HTTP requests to an API updating the outputted
|
// HttpAcmeProvider sends HTTP requests to an API updating the outputted
|
||||||
// `.wellknown/acme-challenges` data
|
// `.wellknown/acme-challenges` data
|
||||||
type HttpAcmeProvider struct {
|
type HttpAcmeProvider struct {
|
||||||
|
tokenFile string
|
||||||
accessToken, refreshToken string
|
accessToken, refreshToken string
|
||||||
apiUrlPresent, apiUrlCleanUp string
|
apiUrlPresent, apiUrlCleanUp string
|
||||||
apiUrlRefreshToken string
|
apiUrlRefreshToken string
|
||||||
trip http.RoundTripper
|
trip http.RoundTripper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AcmeLogin struct {
|
||||||
|
Access string `yaml:"access"`
|
||||||
|
Refresh string `yaml:"refresh"`
|
||||||
|
}
|
||||||
|
|
||||||
// NewHttpAcmeProvider creates a new HttpAcmeProvider using http.DefaultTransport
|
// NewHttpAcmeProvider creates a new HttpAcmeProvider using http.DefaultTransport
|
||||||
// as the transport
|
// as the transport
|
||||||
func NewHttpAcmeProvider(accessToken, refreshToken, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken string) *HttpAcmeProvider {
|
func NewHttpAcmeProvider(tokenFile, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken string) (*HttpAcmeProvider, error) {
|
||||||
return &HttpAcmeProvider{accessToken, refreshToken, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken, http.DefaultTransport}
|
// acme login token
|
||||||
|
openTokens, err := os.Open(tokenFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load acme tokens: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var acmeLogins AcmeLogin
|
||||||
|
err = yaml.NewDecoder(openTokens).Decode(&acmeLogins)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load acme tokens: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HttpAcmeProvider{tokenFile, acmeLogins.Access, acmeLogins.Refresh, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken, http.DefaultTransport}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present implements challenge.Provider and sends a put request to the specified
|
// Present implements challenge.Provider and sends a put request to the specified
|
||||||
@ -93,6 +114,8 @@ func (h *HttpAcmeProvider) authCheckRequest(method, url, domain, token, keyAuth
|
|||||||
h.accessToken = tokens.Access
|
h.accessToken = tokens.Access
|
||||||
h.refreshToken = tokens.Refresh
|
h.refreshToken = tokens.Refresh
|
||||||
|
|
||||||
|
go h.saveLoginTokens()
|
||||||
|
|
||||||
// call internal request again
|
// call internal request again
|
||||||
resp, err = h.internalRequest(method, url, domain, token, keyAuth)
|
resp, err = h.internalRequest(method, url, domain, token, keyAuth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -110,11 +133,25 @@ func (h *HttpAcmeProvider) authCheckRequest(method, url, domain, token, keyAuth
|
|||||||
|
|
||||||
// internalRequest sends a request to the acme challenge hosting api
|
// internalRequest sends a request to the acme challenge hosting api
|
||||||
func (h *HttpAcmeProvider) internalRequest(method, url, domain, token, keyAuth string) (*http.Response, error) {
|
func (h *HttpAcmeProvider) internalRequest(method, url, domain, token, keyAuth string) (*http.Response, error) {
|
||||||
v := strings.NewReplacer("%domain%", domain, "%token%", token, "%content%", keyAuth).Replace(url)
|
v := strings.NewReplacer("$domain", domain, "$token", token, "$content", keyAuth).Replace(url)
|
||||||
req, err := http.NewRequest(method, v, nil)
|
req, err := http.NewRequest(method, v, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+h.accessToken)
|
req.Header.Set("Authorization", "Bearer "+h.accessToken)
|
||||||
return h.trip.RoundTrip(req)
|
return h.trip.RoundTrip(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HttpAcmeProvider) saveLoginTokens() {
|
||||||
|
// acme login token
|
||||||
|
openTokens, err := os.Create(h.tokenFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[Orchid] Failed to open token file:", err)
|
||||||
|
}
|
||||||
|
defer openTokens.Close()
|
||||||
|
|
||||||
|
err = yaml.NewEncoder(openTokens).Encode(AcmeLogin{Access: h.accessToken, Refresh: h.refreshToken})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[Orchid] Failed to write tokens file:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -18,10 +18,11 @@ import (
|
|||||||
|
|
||||||
func makeQuickHttpProv(accessToken string, ft http.RoundTripper) *HttpAcmeProvider {
|
func makeQuickHttpProv(accessToken string, ft http.RoundTripper) *HttpAcmeProvider {
|
||||||
return &HttpAcmeProvider{
|
return &HttpAcmeProvider{
|
||||||
|
"",
|
||||||
accessToken,
|
accessToken,
|
||||||
"",
|
"",
|
||||||
"https://api.example.com/acme/present/%domain%/%token%/%content%",
|
"https://api.example.com/acme/present/$domain/$token/$content",
|
||||||
"https://api.example.com/acme/clean/%domain%/%token%",
|
"https://api.example.com/acme/clean/$domain/$token",
|
||||||
"https://api.example.com/acme/token",
|
"https://api.example.com/acme/token",
|
||||||
ft,
|
ft,
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package renewal
|
package renewal
|
||||||
|
|
||||||
type LetsEncryptConfig struct {
|
type LetsEncryptConfig struct {
|
||||||
Account struct {
|
Account LetsEncryptAccount `yaml:"account"`
|
||||||
Email string `yaml:"email"`
|
|
||||||
PrivateKey string `yaml:"privateKey"`
|
|
||||||
} `yaml:"account"`
|
|
||||||
Directory string `yaml:"directory"`
|
Directory string `yaml:"directory"`
|
||||||
Certificate string `yaml:"certificate"`
|
Certificate string `yaml:"certificate"`
|
||||||
insecure bool
|
insecure bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LetsEncryptAccount struct {
|
||||||
|
Email string `yaml:"email"`
|
||||||
|
PrivateKey string `yaml:"key"`
|
||||||
|
}
|
||||||
|
@ -124,13 +124,16 @@ func (s *Service) Shutdown() {
|
|||||||
// resolveLEPrivKey resolves the private key for the LetsEncrypt account.
|
// resolveLEPrivKey resolves the private key for the LetsEncrypt account.
|
||||||
// If the string is a path to a file then the contents of the file is read.
|
// If the string is a path to a file then the contents of the file is read.
|
||||||
func (s *Service) resolveLEPrivKey(a string) error {
|
func (s *Service) resolveLEPrivKey(a string) error {
|
||||||
key, err := x509.ParsePKCS1PrivateKey([]byte(a))
|
p, _ := pem.Decode([]byte(a))
|
||||||
if err != nil {
|
if p == nil {
|
||||||
raw, err := os.ReadFile(a)
|
return fmt.Errorf("failed to parse pem encoding")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
key, err = x509.ParsePKCS1PrivateKey(raw)
|
if p.Type != "RSA PRIVATE KEY" {
|
||||||
|
return fmt.Errorf("invalid key types: %s", p.Type)
|
||||||
|
}
|
||||||
|
key, err := x509.ParsePKCS1PrivateKey(p.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse key: %w", err)
|
||||||
}
|
}
|
||||||
s.leAccount.key = key
|
s.leAccount.key = key
|
||||||
return err
|
return err
|
||||||
|
@ -122,10 +122,7 @@ func setupPebbleTest(t *testing.T, serverTls *certgen.CertGen) *Service {
|
|||||||
|
|
||||||
acmeProv := test.MakeFakeAcmeProv(serverTls.GetCertPem())
|
acmeProv := test.MakeFakeAcmeProv(serverTls.GetCertPem())
|
||||||
service, err := NewService(wg, db, acmeProv, LetsEncryptConfig{
|
service, err := NewService(wg, db, acmeProv, LetsEncryptConfig{
|
||||||
Account: struct {
|
Account: LetsEncryptAccount{
|
||||||
Email string `yaml:"email"`
|
|
||||||
PrivateKey string `yaml:"privateKey"`
|
|
||||||
}{
|
|
||||||
Email: "webmaster@example.test",
|
Email: "webmaster@example.test",
|
||||||
PrivateKey: string(x509.MarshalPKCS1PrivateKey(lePrivKey)),
|
PrivateKey: string(x509.MarshalPKCS1PrivateKey(lePrivKey)),
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user