jx/internal/resource/pki.go
Matthew Rich 8feb7b8d56
Some checks failed
Lint / golangci-lint (push) Failing after 10m1s
Declarative Tests / test (push) Failing after 14s
add support of import search paths [doublejynx/jx#7]
2024-10-16 10:26:42 -07:00

508 lines
14 KiB
Go

// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
"errors"
"fmt"
"log/slog"
"gopkg.in/yaml.v3"
"net/url"
"path/filepath"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"decl/internal/ext"
"decl/internal/transport"
"decl/internal/data"
"decl/internal/folio"
"crypto/x509"
"crypto/x509/pkix"
"crypto/rsa"
"crypto/rand"
"encoding/pem"
"encoding/json"
"time"
"math/big"
"io"
"strings"
)
const (
PKITypeName TypeName = "pki"
)
// Describes the type of certificate file the resource represents
type EncodingType string
// Supported file types
const (
EncodingTypePem EncodingType = "pem"
)
var ErrPKIInvalidEncodingType error = errors.New("Invalid EncodingType")
var ErrPKIFailedDecodingPemBlock error = errors.New("Failed decoding pem block")
func init() {
ResourceTypes.Register([]string{"pki"}, func(u *url.URL) (data.Resource) {
k := NewPKI()
if u != nil {
if err := folio.CastParsedURI(u).ConstructResource(k); err != nil {
panic(err)
}
ref := folio.ResourceReference(filepath.Join(u.Hostname(), u.Path))
if len(ref) > 0 {
k.PrivateKeyRef = ref
}
}
return k
})
}
type Certificate struct {
*x509.Certificate `yaml:",inline" json:",inline"`
Raw []byte `yaml:"-" json:"-"`
RawTBSCertificate []byte `yaml:"-" json:"-"`
RawSubjectPublicKeyInfo []byte `yaml:"-" json:"-"`
RawSubject []byte `yaml:"-" json:"-"`
RawIssuer []byte `yaml:"-" json:"-"`
}
type PKI struct {
*Common `json:",inline" yaml:",inline"`
stater machine.Stater `json:"-" yaml:"-"`
PrivateKeyPem string `json:"privatekey,omitempty" yaml:"privatekey,omitempty"`
PublicKeyPem string `json:"publickey,omitempty" yaml:"publickey,omitempty"`
CertificatePem string `json:"certificate,omitempty" yaml:"certificate,omitempty"`
PrivateKeyRef folio.ResourceReference `json:"privatekeyref,omitempty" yaml:"privatekeyref,omitempty"` // Describes a resource URI to read/write the private key content
PublicKeyRef folio.ResourceReference `json:"publickeyref,omitempty" yaml:"publickeyref,omitempty"` // Describes a resource URI to read/write the public key content
CertificateRef folio.ResourceReference `json:"certificateref,omitempty" yaml:"certificateref,omitempty"` // Describes a resource URI to read/write the certificate content
SignedByRef folio.ResourceReference `json:"signedbyref,omitempty" yaml:"signedbyref,omitempty"` // Describes a resource URI for the signing cert
privateKey *rsa.PrivateKey `json:"-" yaml:"-"`
publicKey *rsa.PublicKey `json:"-" yaml:"-"`
Values *Certificate `json:"values,omitempty" yaml:"values,omitempty"`
Certificate *x509.Certificate `json:"-" yaml:"-"`
Bits int `json:"bits" yaml:"bits"`
EncodingType EncodingType `json:"type" yaml:"type"`
Resources data.ResourceMapper `json:"-" yaml:"-"`
}
func NewPKI() *PKI {
p := &PKI{ EncodingType: EncodingTypePem, Bits: 2048, Common: NewCommon(PKITypeName, false) }
return p
}
func (k *PKI) SetResourceMapper(resources data.ResourceMapper) {
slog.Info("PKI.SetResourceMapper()", "resources", resources)
k.Resources = resources
}
func (k *PKI) Clone() data.Resource {
return &PKI {
Common: k.Common.Clone(),
EncodingType: k.EncodingType,
}
}
func (k *PKI) StateMachine() machine.Stater {
if k.stater == nil {
k.stater = StorageMachine(k)
}
return k.stater
}
func (k *PKI) Notify(m *machine.EventMessage) {
ctx := context.Background()
slog.Info("Notify()", k.Type(), k, "m", m)
switch m.On {
case machine.ENTERSTATEEVENT:
switch m.Dest {
case "start_stat":
if statErr := k.ReadStat(); statErr == nil {
if triggerErr := k.StateMachine().Trigger("exists"); triggerErr == nil {
return
}
} else {
if triggerErr := k.StateMachine().Trigger("notexists"); triggerErr == nil {
return
}
}
case "start_read":
if _,readErr := k.Read(ctx); readErr == nil {
if triggerErr := k.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else {
//k.State = "absent"
panic(triggerErr)
}
} else {
//k.State = "absent"
panic(readErr)
}
case "start_create":
if ! (k.PrivateKeyRef.Exists() || k.PublicKeyRef.Exists() || k.CertificateRef.Exists()) {
if e := k.Create(ctx); e == nil {
if triggerErr := k.StateMachine().Trigger("created"); triggerErr == nil {
return
}
}
}
//k.State = "absent"
case "start_update":
if e := k.Update(ctx); e == nil {
if triggerErr := k.StateMachine().Trigger("updated"); triggerErr == nil {
return
}
}
case "start_delete":
if deleteErr := k.Delete(ctx); deleteErr == nil {
if triggerErr := k.StateMachine().Trigger("deleted"); triggerErr == nil {
return
} else {
//k.State = "present"
panic(triggerErr)
}
} else {
//k.State = "present"
panic(deleteErr)
}
case "absent":
//k.State = "absent"
case "present", "updated", "created", "read":
//k.State = "present"
}
case machine.EXITSTATEEVENT:
}
}
func (k *PKI) URI() string {
u := k.PrivateKeyRef.Parse()
if u.Scheme == "file" || u.Scheme == "pki" {
return fmt.Sprintf("pki://%s", filepath.Join(u.Hostname(), u.Path))
}
return fmt.Sprintf("pki://%s", filepath.Join(u.Hostname(), u.RequestURI()))
}
func (k *PKI) Validate() error {
return fmt.Errorf("failed")
}
func (k *PKI) Apply() error {
/*
ctx := context.Background()
switch k.State {
case "absent":
return k.Delete(ctx)
case "present":
return k.Create(ctx)
}
*/
return nil
}
func (k *PKI) Load(docData []byte, f codec.Format) (err error) {
err = f.StringDecoder(string(docData)).Decode(k)
return
}
func (k *PKI) LoadReader(r io.ReadCloser, f codec.Format) (err error) {
err = f.Decoder(r).Decode(k)
return
}
func (k *PKI) LoadString(docData string, f codec.Format) (err error) {
err = f.StringDecoder(docData).Decode(k)
return
}
func (k *PKI) LoadDecl(yamlResourceDeclaration string) error {
return k.LoadString(yamlResourceDeclaration, codec.FormatYaml)
}
func (k *PKI) ReadStat() (err error) {
var resourcesErr []string
if ! k.PrivateKeyRef.Exists() {
resourcesErr = append(resourcesErr, string(k.PrivateKeyRef))
}
if ! k.PublicKeyRef.Exists() {
resourcesErr = append(resourcesErr, string(k.PublicKeyRef))
}
if ! k.CertificateRef.Exists() {
resourcesErr = append(resourcesErr, string(k.CertificateRef))
}
if len(resourcesErr) > 0 {
err = fmt.Errorf("PKI resources missing: %s", strings.Join(resourcesErr, ","))
// k.State = "absent"
}
return
}
func (k *PKI) ResolveId(ctx context.Context) string {
return string(k.PrivateKeyRef)
}
func (k *PKI) GenerateKey() (err error) {
k.privateKey, err = rsa.GenerateKey(rand.Reader, k.Bits)
return
}
func (k *PKI) PublicKey() {
if k.privateKey != nil {
k.publicKey = k.privateKey.Public().(*rsa.PublicKey)
}
}
func (k *PKI) Encode() {
var privFileStream, pubFileStream io.WriteCloser
switch k.EncodingType {
case EncodingTypePem:
if len(k.PrivateKeyRef) > 0 {
privFileStream, _ = k.PrivateKeyRef.Lookup(k.Resources).ContentWriterStream()
} else {
var privateKeyContent strings.Builder
privFileStream = ext.WriteNopCloser(&privateKeyContent)
defer func() { k.PrivateKeyPem = privateKeyContent.String() }()
}
defer privFileStream.Close()
privEncodeErr := pem.Encode(privFileStream, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k.privateKey)})
if privEncodeErr != nil {
panic(privEncodeErr)
}
if len(k.PublicKeyRef) > 0 {
pubFileStream, _ = k.PublicKeyRef.Lookup(k.Resources).ContentWriterStream()
} else {
var publicKeyContent strings.Builder
pubFileStream = ext.WriteNopCloser(&publicKeyContent)
defer func() { k.PublicKeyPem = publicKeyContent.String() }()
}
defer pubFileStream.Close()
pubEncodeErr := pem.Encode(pubFileStream, &pem.Block{Type: "RSA PUBLIC KEY", Bytes: x509.MarshalPKCS1PublicKey(k.publicKey)})
if pubEncodeErr != nil {
panic(pubEncodeErr)
}
}
}
func (k *PKI) Decode() {
slog.Info("PKI.Decode()", "privatekey", k.PrivateKeyRef, "publickey", k.PublicKeyRef, "certificate", k.CertificateRef)
var err error
switch k.EncodingType {
case EncodingTypePem:
if len(k.PrivateKeyRef) > 0 && k.PrivateKeyRef.Exists() {
privReader := k.PrivateKeyRef.Lookup(k.Resources)
privFileStream, _ := privReader.ContentReaderStream()
defer privFileStream.Close()
PrivateKeyPemData, readErr := io.ReadAll(privFileStream)
if readErr != nil {
panic(readErr)
}
k.PrivateKeyPem = string(PrivateKeyPemData)
block, _ := pem.Decode(PrivateKeyPemData)
if block != nil {
k.privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
panic(err)
}
} else {
panic(ErrPKIFailedDecodingPemBlock)
}
}
slog.Info("PKI.Decode() decoded private key", "error", err)
if len(k.PublicKeyRef) > 0 && k.PublicKeyRef.Exists() {
pubReader := k.PublicKeyRef.Lookup(k.Resources)
pubFileStream, _ := pubReader.ContentReaderStream()
defer pubFileStream.Close()
PublicKeyPemData, readErr := io.ReadAll(pubFileStream)
if readErr != nil {
panic(err)
}
k.PublicKeyPem = string(PublicKeyPemData)
block, _ := pem.Decode(PublicKeyPemData)
if block != nil {
k.publicKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
if err != nil {
panic(err)
}
} else {
panic(ErrPKIFailedDecodingPemBlock)
}
}
slog.Info("PKI.Decode() decoded public key", "publickey", k.PublicKeyPem, "error", err)
if len(k.CertificateRef) > 0 && k.CertificateRef.Exists() {
certReader := k.CertificateRef.Lookup(k.Resources)
certFileStream, _ := certReader.ContentReaderStream()
if certFileStream != nil {
defer certFileStream.Close()
CertificatePemData, readErr := io.ReadAll(certFileStream)
if readErr != nil {
panic(readErr)
}
slog.Info("PKI.Decode() certificate", "pem", CertificatePemData, "error", err)
k.CertificatePem = string(CertificatePemData)
block, _ := pem.Decode(CertificatePemData)
if block != nil {
k.Certificate, err = x509.ParseCertificate(block.Bytes)
if err != nil {
panic(err)
}
} else {
panic(ErrPKIFailedDecodingPemBlock)
}
}
}
}
slog.Info("PKI.Decode()", "error", err)
}
func (k *PKI) ContentReaderStream() (*transport.Reader, error) {
return nil, nil
}
func (k *PKI) ContentWriterStream() (*transport.Writer, error) {
return nil, nil
}
func (k *PKI) SignedBy() (cert *x509.Certificate, pub *rsa.PublicKey, priv *rsa.PrivateKey) {
if len(k.SignedByRef) > 0 {
r := k.SignedByRef.Dereference(k.Resources)
if r != nil {
pkiResource := r.(*PKI)
return pkiResource.Certificate, pkiResource.publicKey, pkiResource.privateKey
}
}
return nil, nil, nil
}
func (k *PKI) CertConfig() (certTemplate *x509.Certificate) {
if k.Values != nil {
certTemplate = k.Values.Certificate
} else {
var caEpoch int64 = 1721005200000
certTemplate = &x509.Certificate {
SerialNumber: big.NewInt(time.Now().UnixMilli() - caEpoch),
Subject: pkix.Name {
Organization: []string{""},
Country: []string{""},
Province: []string{""},
Locality: []string{""},
StreetAddress: []string{""},
PostalCode: []string{""},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
}
if k.config != nil {
if value, configErr := k.config.GetValue("certtemplate"); configErr == nil {
certTemplate = value.(*x509.Certificate)
}
}
slog.Info("PKI.CertConfig()", "template", certTemplate)
return
}
func (k *PKI) GenerateCertificate() (error) {
var signingCert *x509.Certificate
var signingPubKey *rsa.PublicKey
var signingPrivKey *rsa.PrivateKey
var certFileStream io.WriteCloser
certTemplate := k.CertConfig()
signingCert, signingPubKey, signingPrivKey = k.SignedBy()
if signingCert != nil && signingPubKey != nil && signingPrivKey != nil {
slog.Info("PKI.Certificate()", "signedby", k.SignedByRef)
} else {
signingCert = certTemplate
signingPubKey = k.publicKey
signingPrivKey = k.privateKey
}
if k.Certificate != nil {
certTemplate = k.Certificate
}
cert, err := x509.CreateCertificate(rand.Reader, certTemplate, signingCert, signingPubKey, signingPrivKey)
if err != nil {
slog.Error("PKI.Certificate() - x509.CreateCertificate", "cert", cert, "error", err)
return err
}
if len(k.CertificateRef) > 0 {
certFileStream, _ = k.CertificateRef.Lookup(k.Resources).ContentWriterStream()
} else {
var certContent strings.Builder
certFileStream = ext.WriteNopCloser(&certContent)
defer func() { k.CertificatePem = certContent.String() }()
}
defer certFileStream.Close()
certEncodeErr := pem.Encode(certFileStream, &pem.Block{Type: "CERTIFICATE", Bytes: cert})
return certEncodeErr
}
func (k *PKI) Create(ctx context.Context) (err error) {
if err = k.GenerateKey(); err == nil {
k.PublicKey()
k.Encode()
if err = k.GenerateCertificate(); err == nil {
return
}
}
return
}
func (f *PKI) Delete(ctx context.Context) error {
return nil
}
func (k *PKI) Read(ctx context.Context) ([]byte, error) {
k.Decode()
return yaml.Marshal(k)
}
func (k *PKI) Update(ctx context.Context) (err error) {
if err = k.GenerateKey(); err == nil {
k.PublicKey()
k.Encode()
if err = k.GenerateCertificate(); err == nil {
return
}
}
return
}
func (k *PKI) Type() string { return "pki" }
func (t *EncodingType) UnmarshalValue(value string) error {
switch value {
case string(EncodingTypePem):
*t = EncodingType(value)
return nil
default:
return ErrPKIInvalidEncodingType
}
}
func (t *EncodingType) UnmarshalJSON(data []byte) error {
var s string
if unmarshalEncodingTypeErr := json.Unmarshal(data, &s); unmarshalEncodingTypeErr != nil {
return unmarshalEncodingTypeErr
}
return t.UnmarshalValue(s)
}
func (t *EncodingType) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
return t.UnmarshalValue(s)
}