mirror of https://github.com/docker/docs.git
Added cliCryptoService
This commit is contained in:
parent
ff169897b6
commit
712ff83945
|
@ -0,0 +1,130 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/docker/vetinari/trustmanager"
|
||||||
|
"github.com/endophage/gotuf/data"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cliCryptoService struct {
|
||||||
|
privateKeys map[string]*data.PrivateKey
|
||||||
|
gun string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCryptoService(gun string) *cliCryptoService {
|
||||||
|
return &cliCryptoService{privateKeys: make(map[string]*data.PrivateKey), gun: gun}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create is used to generate keys for targets, snapshots and timestamps
|
||||||
|
func (ccs *cliCryptoService) Create() (*data.PublicKey, error) {
|
||||||
|
_, cert, err := generateKeyAndCert(ccs.gun)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.NewPublicKey("RSA", string(cert)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign returns the signatures for data with the given keyIDs
|
||||||
|
func (ccs *cliCryptoService) Sign(keyIDs []string, payload []byte) ([]data.Signature, error) {
|
||||||
|
// Create hasher and hash data
|
||||||
|
hash := crypto.SHA256
|
||||||
|
hashed := sha256.Sum256(payload)
|
||||||
|
|
||||||
|
signatures := make([]data.Signature, 0, len(keyIDs))
|
||||||
|
for _, kID := range keyIDs {
|
||||||
|
// Get the PrivateKey filename
|
||||||
|
privKeyFilename := filepath.Join(viper.GetString("privDir"), ccs.gun, kID+".key")
|
||||||
|
// Read PrivateKey from file
|
||||||
|
privPEMBytes, err := ioutil.ReadFile(privKeyFilename)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse PrivateKey
|
||||||
|
privKeyBytes, _ := pem.Decode(privPEMBytes)
|
||||||
|
privKey, err := x509.ParsePKCS1PrivateKey(privKeyBytes.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign the data
|
||||||
|
sig, err := rsa.SignPKCS1v15(rand.Reader, privKey, hash, hashed[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append signatures to result array
|
||||||
|
signatures = append(signatures, data.Signature{
|
||||||
|
KeyID: kID,
|
||||||
|
Method: "RSASSA-PKCS1-V1_5-SIGN",
|
||||||
|
Signature: sig[:],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return signatures, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO (diogo): Add support for EC P384
|
||||||
|
func generateKeyAndCert(gun string) (crypto.PrivateKey, []byte, error) {
|
||||||
|
|
||||||
|
// Generates a new RSA key
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("could not generate private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBytes := x509.MarshalPKCS1PrivateKey(key)
|
||||||
|
|
||||||
|
// Creates a new Certificate template. We need the certificate to calculate the
|
||||||
|
// TUF-compliant keyID
|
||||||
|
//TODO (diogo): We're hardcoding the Organization to be the GUN. Probably want to
|
||||||
|
// change it
|
||||||
|
template := newCertificate(gun, gun)
|
||||||
|
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to generate the certificate for key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the new certificate into PEM
|
||||||
|
cert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||||
|
|
||||||
|
// Create new TUF Key so we can compute the TUF-compliant ID
|
||||||
|
tufKey := data.NewTUFKey("RSA", string(cert), "")
|
||||||
|
|
||||||
|
// The key is going to be stored in the private directory, using the GUN and
|
||||||
|
// the filename will be the TUF-compliant ID
|
||||||
|
privKeyFilename := filepath.Join(viper.GetString("privDir"), gun, tufKey.ID()+".key")
|
||||||
|
|
||||||
|
// If GUN is in the form of 'foo/bar' ensures that private key is stored in the
|
||||||
|
// adequate sub-directory
|
||||||
|
err = trustmanager.CreateDirectory(privKeyFilename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("could not create directory for private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opens a FD to the file with the correct permissions
|
||||||
|
keyOut, err := os.OpenFile(privKeyFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("could not write privatekey: %v", err)
|
||||||
|
}
|
||||||
|
defer keyOut.Close()
|
||||||
|
|
||||||
|
// Encodes the private key as PEM and writes it
|
||||||
|
err = pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to encode key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, cert, nil
|
||||||
|
}
|
|
@ -1,24 +1,19 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/vetinari/trustmanager"
|
"github.com/docker/vetinari/trustmanager"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var subjectKeyID string
|
var subjectKeyID string
|
||||||
|
@ -99,7 +94,7 @@ func keysTrust(cmd *cobra.Command, args []string) {
|
||||||
fatalf("not enough arguments provided")
|
fatalf("not enough arguments provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
qualifiedDN := args[0]
|
gun := args[0]
|
||||||
certLocationStr := args[1]
|
certLocationStr := args[1]
|
||||||
// Verify if argument is a valid URL
|
// Verify if argument is a valid URL
|
||||||
url, err := url.Parse(certLocationStr)
|
url, err := url.Parse(certLocationStr)
|
||||||
|
@ -109,9 +104,9 @@ func keysTrust(cmd *cobra.Command, args []string) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf("error retreiving certificate from url (%s): %v", certLocationStr, err)
|
fatalf("error retreiving certificate from url (%s): %v", certLocationStr, err)
|
||||||
}
|
}
|
||||||
err = cert.VerifyHostname(qualifiedDN)
|
err = cert.VerifyHostname(gun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf("certificate does not match the Qualified Docker Name: %v", err)
|
fatalf("certificate does not match the Global Unique Name: %v", err)
|
||||||
}
|
}
|
||||||
err = caStore.AddCert(cert)
|
err = caStore.AddCert(cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -156,33 +151,17 @@ func keysGenerate(cmd *cobra.Command, args []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// (diogo): Validate GUNs
|
// (diogo): Validate GUNs
|
||||||
qualifiedDN := args[0]
|
gun := args[0]
|
||||||
|
|
||||||
key, err := generateKey(qualifiedDN)
|
_, cert, err := generateKeyAndCert(gun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf("could not generate key: %v", err)
|
fatalf("could not generate key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
template := newCertificate(qualifiedDN, qualifiedDN)
|
caStore.AddCertFromPEM(cert)
|
||||||
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.(crypto.Signer).Public(), key)
|
|
||||||
if err != nil {
|
|
||||||
fatalf("failed to generate certificate: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
certName := filepath.Join(viper.GetString("privDir"), qualifiedDN+".crt")
|
|
||||||
certOut, err := os.Create(certName)
|
|
||||||
if err != nil {
|
|
||||||
fatalf("failed to save certificate: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer certOut.Close()
|
|
||||||
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
|
||||||
if err != nil {
|
|
||||||
fatalf("failed to save certificate: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCertificate(qualifiedDN, organization string) *x509.Certificate {
|
func newCertificate(gun, organization string) *x509.Certificate {
|
||||||
notBefore := time.Now()
|
notBefore := time.Now()
|
||||||
notAfter := notBefore.Add(time.Hour * 24 * 365 * 2)
|
notAfter := notBefore.Add(time.Hour * 24 * 365 * 2)
|
||||||
|
|
||||||
|
@ -196,7 +175,7 @@ func newCertificate(qualifiedDN, organization string) *x509.Certificate {
|
||||||
SerialNumber: serialNumber,
|
SerialNumber: serialNumber,
|
||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
Organization: []string{organization},
|
Organization: []string{organization},
|
||||||
CommonName: qualifiedDN,
|
CommonName: gun,
|
||||||
},
|
},
|
||||||
NotBefore: notBefore,
|
NotBefore: notBefore,
|
||||||
NotAfter: notAfter,
|
NotAfter: notAfter,
|
||||||
|
@ -207,33 +186,6 @@ func newCertificate(qualifiedDN, organization string) *x509.Certificate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateKey(qualifiedDN string) (crypto.PrivateKey, error) {
|
|
||||||
curve := elliptic.P384()
|
|
||||||
key, err := ecdsa.GenerateKey(curve, rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not generate private key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keyBytes, err := x509.MarshalECPrivateKey(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not marshal private key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keyName := filepath.Join(viper.GetString("privDir"), qualifiedDN+".key")
|
|
||||||
keyOut, err := os.OpenFile(keyName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not write privatekey: %v", err)
|
|
||||||
}
|
|
||||||
defer keyOut.Close()
|
|
||||||
|
|
||||||
err = pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to encode key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func printCert(cert *x509.Certificate) {
|
func printCert(cert *x509.Certificate) {
|
||||||
timeDifference := cert.NotAfter.Sub(time.Now())
|
timeDifference := cert.NotAfter.Sub(time.Now())
|
||||||
subjectKeyID := trustmanager.FingerprintCert(cert)
|
subjectKeyID := trustmanager.FingerprintCert(cert)
|
||||||
|
|
|
@ -15,9 +15,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const configFileName string = "config"
|
const configFileName string = "config"
|
||||||
|
|
||||||
|
// Default paths should end with a '/' so directory creation works correctly
|
||||||
const configPath string = ".docker/trust/"
|
const configPath string = ".docker/trust/"
|
||||||
const caDir string = ".docker/trust/certificate_authorities/"
|
const trustDir string = configPath + "repository_certificates/"
|
||||||
const privDir string = ".docker/trust/private/"
|
const privDir string = configPath + "private/"
|
||||||
|
|
||||||
var caStore trustmanager.X509Store
|
var caStore trustmanager.X509Store
|
||||||
var privStore trustmanager.X509Store
|
var privStore trustmanager.X509Store
|
||||||
|
@ -45,26 +47,32 @@ func init() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Ignore if the configuration file doesn't exist, we can use the defaults
|
// Ignore if the configuration file doesn't exist, we can use the defaults
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
panic(fmt.Errorf("Fatal error config file: %s \n", err))
|
fatalf("fatal error config file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the defaults for our config
|
// Set up the defaults for our config
|
||||||
viper.SetDefault("caDir", path.Join(homeDir, path.Dir(caDir)))
|
viper.SetDefault("trustDir", path.Join(homeDir, path.Dir(trustDir)))
|
||||||
viper.SetDefault("privDir", path.Join(homeDir, path.Dir(privDir)))
|
viper.SetDefault("privDir", path.Join(homeDir, path.Dir(privDir)))
|
||||||
|
|
||||||
// Get the final value for the CA directory
|
// Get the final value for the CA directory
|
||||||
finalcaDir := viper.GetString("caDir")
|
finalTrustDir := viper.GetString("trustDir")
|
||||||
finalPrivDir := viper.GetString("privDir")
|
finalPrivDir := viper.GetString("privDir")
|
||||||
|
|
||||||
// Ensure the existence of the CAs directory
|
// Ensure the existence of the CAs directory
|
||||||
createDirectory(finalcaDir)
|
err = trustmanager.CreateDirectory(finalTrustDir)
|
||||||
createDirectory(finalPrivDir)
|
if err != nil {
|
||||||
|
fatalf("could not create directory: %v", err)
|
||||||
|
}
|
||||||
|
err = trustmanager.CreateDirectory(finalPrivDir)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("could not create directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Load all CAs that aren't expired and don't use SHA1
|
// Load all CAs that aren't expired and don't use SHA1
|
||||||
// We could easily add "return cert.IsCA && cert.BasicConstraintsValid" in order
|
// We could easily add "return cert.IsCA && cert.BasicConstraintsValid" in order
|
||||||
// to have only valid CA certificates being loaded
|
// to have only valid CA certificates being loaded
|
||||||
caStore = trustmanager.NewX509FilteredFileStore(finalcaDir, func(cert *x509.Certificate) bool {
|
caStore = trustmanager.NewX509FilteredFileStore(finalTrustDir, func(cert *x509.Certificate) bool {
|
||||||
return time.Now().Before(cert.NotAfter) &&
|
return time.Now().Before(cert.NotAfter) &&
|
||||||
cert.SignatureAlgorithm != x509.SHA1WithRSA &&
|
cert.SignatureAlgorithm != x509.SHA1WithRSA &&
|
||||||
cert.SignatureAlgorithm != x509.DSAWithSHA1 &&
|
cert.SignatureAlgorithm != x509.DSAWithSHA1 &&
|
||||||
|
@ -95,9 +103,3 @@ func fatalf(format string, args ...interface{}) {
|
||||||
fmt.Printf("* fatal: "+format+"\n", args...)
|
fmt.Printf("* fatal: "+format+"\n", args...)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDirectory(dir string) {
|
|
||||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
||||||
fatalf("cannot create directory: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -48,35 +48,35 @@ var cmdTufAdd = &cobra.Command{
|
||||||
var cmdTufRemove = &cobra.Command{
|
var cmdTufRemove = &cobra.Command{
|
||||||
Use: "remove [ GUN ] <target>",
|
Use: "remove [ GUN ] <target>",
|
||||||
Short: "Removes a target from the TUF repo.",
|
Short: "Removes a target from the TUF repo.",
|
||||||
Long: "removes a target from the local TUF repo identified by a Qualified Docker Name.",
|
Long: "removes a target from the local TUF repo identified by a Global Unique Name.",
|
||||||
Run: tufRemove,
|
Run: tufRemove,
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdTufInit = &cobra.Command{
|
var cmdTufInit = &cobra.Command{
|
||||||
Use: "init [ GUN ]",
|
Use: "init [ GUN ]",
|
||||||
Short: "initializes the local TUF repository.",
|
Short: "initializes the local TUF repository.",
|
||||||
Long: "creates locally the initial set of TUF metadata for the Qualified Docker Name.",
|
Long: "creates locally the initial set of TUF metadata for the Global Unique Name.",
|
||||||
Run: tufInit,
|
Run: tufInit,
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdTufList = &cobra.Command{
|
var cmdTufList = &cobra.Command{
|
||||||
Use: "list [ GUN ]",
|
Use: "list [ GUN ]",
|
||||||
Short: "Lists all targets in a TUF repository.",
|
Short: "Lists all targets in a TUF repository.",
|
||||||
Long: "lists all the targets in the TUF repository identified by the Qualified Docker Name.",
|
Long: "lists all the targets in the TUF repository identified by the Global Unique Name.",
|
||||||
Run: tufList,
|
Run: tufList,
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdTufLookup = &cobra.Command{
|
var cmdTufLookup = &cobra.Command{
|
||||||
Use: "lookup [ GUN ] <target name>",
|
Use: "lookup [ GUN ] <target name>",
|
||||||
Short: "Looks up a specific TUF target in a repository.",
|
Short: "Looks up a specific TUF target in a repository.",
|
||||||
Long: "looks up a TUF target in a repository given a Qualified Docker Name.",
|
Long: "looks up a TUF target in a repository given a Global Unique Name.",
|
||||||
Run: tufLookup,
|
Run: tufLookup,
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdTufPush = &cobra.Command{
|
var cmdTufPush = &cobra.Command{
|
||||||
Use: "push [ GUN ]",
|
Use: "push [ GUN ]",
|
||||||
Short: "initializes the local TUF repository.",
|
Short: "initializes the local TUF repository.",
|
||||||
Long: "creates locally the initial set of TUF metadata for the Qualified Docker Name.",
|
Long: "creates locally the initial set of TUF metadata for the Global Unique Name.",
|
||||||
Run: tufPush,
|
Run: tufPush,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,6 +161,7 @@ func tufInit(cmd *cobra.Command, args []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
gun := args[0]
|
gun := args[0]
|
||||||
|
// cryptoService := NewCryptoService(gun)
|
||||||
kdb := keys.NewDB()
|
kdb := keys.NewDB()
|
||||||
repo := tuf.NewTufRepo(kdb, nil)
|
repo := tuf.NewTufRepo(kdb, nil)
|
||||||
|
|
||||||
|
|
|
@ -57,8 +57,14 @@ func (s X509FileStore) AddCert(cert *x509.Certificate) error {
|
||||||
return errors.New("adding nil Certificate to X509Store")
|
return errors.New("adding nil Certificate to X509Store")
|
||||||
}
|
}
|
||||||
|
|
||||||
fingerprint := FingerprintCert(cert)
|
var filename string
|
||||||
filename := path.Join(s.baseDir, string(fingerprint)+certExtension)
|
if cert.Subject.CommonName != "" {
|
||||||
|
filename = path.Join(s.baseDir, cert.Subject.CommonName+certExtension)
|
||||||
|
} else {
|
||||||
|
fingerprint := FingerprintCert(cert)
|
||||||
|
filename = path.Join(s.baseDir, string(fingerprint)+certExtension)
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.addNamedCert(cert, filename); err != nil {
|
if err := s.addNamedCert(cert, filename); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
@ -54,7 +55,12 @@ func saveCertificate(cert *x509.Certificate, filename string) error {
|
||||||
block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
|
block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
|
||||||
pemdata := string(pem.EncodeToMemory(&block))
|
pemdata := string(pem.EncodeToMemory(&block))
|
||||||
|
|
||||||
err := ioutil.WriteFile(filename, []byte(pemdata), 0600)
|
err := CreateDirectory(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(filename, []byte(pemdata), 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -124,3 +130,11 @@ func loadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) {
|
||||||
|
|
||||||
return nil, errors.New("no certificates found in PEM data")
|
return nil, errors.New("no certificates found in PEM data")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateDirectory(dir string) error {
|
||||||
|
cleanDir := filepath.Dir(dir)
|
||||||
|
if err := os.MkdirAll(cleanDir, 0700); err != nil {
|
||||||
|
return fmt.Errorf("cannot create directory: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue