jx/internal/resource/openpgp_signature.go

366 lines
9.3 KiB
Go
Raw Permalink Normal View History

// 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"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"decl/internal/ext"
"decl/internal/transport"
"decl/internal/data"
"decl/internal/folio"
"crypto"
"encoding/json"
"io"
"io/fs"
"bytes"
"os"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/ProtonMail/go-crypto/openpgp/armor"
)
var (
ErrSignatureWriterFailed error = errors.New("Failed creating signature writer")
ErrArmoredWriterFailed error = errors.New("Failed to create armored writer")
)
const (
OpenPGPSignatureTypeName TypeName = "openpgp-signature"
)
func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"openpgp-signature"}, func(u *url.URL) (res data.Resource) {
o := NewOpenPGPSignature()
if u != nil {
if err := folio.CastParsedURI(u).ConstructResource(o); err != nil {
panic(err)
}
}
return o
})
}
type OpenPGPSignature struct {
*Common `json:",inline" yaml:",inline"`
stater machine.Stater `json:"-" yaml:"-"`
Signature string `json:"signature,omitempty" yaml:"signature,omitempty"`
KeyRingRef folio.ResourceReference `json:"keyringref,omitempty" yaml:"keyringref,omitempty"`
SourceRef folio.ResourceReference `json:"soureref,omitempty" yaml:"sourceref,omitempty"`
SignatureRef folio.ResourceReference `json:"signatureref,omitempty" yaml:"signatureref,omitempty"`
message *openpgp.MessageDetails
entityList openpgp.EntityList
}
func NewOpenPGPSignature() *OpenPGPSignature {
o := &OpenPGPSignature {
Common: NewCommon(OpenPGPSignatureTypeName, false),
}
return o
}
func (o *OpenPGPSignature) Type() string { return "openpgp-signature" }
func (o *OpenPGPSignature) Init(uri data.URIParser) error {
if uri == nil {
uri = folio.URI(o.URI()).Parse()
} else {
// o.Name = uri.URL().Hostname()
}
return o.SetParsedURI(uri)
}
func (o *OpenPGPSignature) NormalizePath() error {
return nil
}
func (o *OpenPGPSignature) StateMachine() machine.Stater {
if o.stater == nil {
o.stater = StorageMachine(o)
}
return o.stater
}
func (o *OpenPGPSignature) URI() string {
return string(o.Common.URI())
}
func (o *OpenPGPSignature) SigningEntity() (err error) {
if o.KeyRingRef.IsEmpty() {
var keyringConfig any
if keyringConfig, err = o.config.GetValue("keyring"); err == nil {
o.entityList = keyringConfig.(openpgp.EntityList)
}
} else {
ringFileStream, _ := o.KeyRingRef.Lookup(o.Resources).ContentReaderStream()
defer ringFileStream.Close()
o.entityList, err = openpgp.ReadArmoredKeyRing(ringFileStream)
}
return
}
func (o *OpenPGPSignature) Sign(message io.Reader, w io.Writer) (err error) {
var writer io.WriteCloser
entity := o.entityList[0]
if writer, err = openpgp.Sign(w, entity, nil, o.Config()); err == nil {
defer writer.Close()
_, err = io.Copy(writer, message)
} else {
err = fmt.Errorf("%w: %w", ErrSignatureWriterFailed, err)
}
return
}
func (o *OpenPGPSignature) Create(ctx context.Context) (err error) {
if err = o.SigningEntity(); err == nil {
var sourceReadStream io.ReadCloser
sourceReadStream, err = o.SourceRef.Lookup(o.Resources).ContentReaderStream()
var signatureStream, armoredWriter io.WriteCloser
if o.SignatureRef.IsEmpty() {
var signatureContent bytes.Buffer
signatureStream = ext.WriteNopCloser(&signatureContent)
defer func() { o.Signature = signatureContent.String() }()
} else {
signatureStream, _ = o.SignatureRef.Lookup(o.Resources).ContentWriterStream()
}
if armoredWriter, err = armor.Encode(signatureStream, openpgp.SignatureType, nil); err != nil {
err = fmt.Errorf("%w: %w", ErrArmoredWriterFailed, err)
}
defer armoredWriter.Close()
err = o.Sign(sourceReadStream, armoredWriter)
}
return
}
func (o *OpenPGPSignature) Validate() (err error) {
var signatureJson []byte
if signatureJson, err = o.JSON(); err == nil {
s := NewSchema(o.Type())
err = s.Validate(string(signatureJson))
}
return err
}
func (o *OpenPGPSignature) Config() *packet.Config {
config := &packet.Config{
RSABits: 2048,
Algorithm: packet.PubKeyAlgoRSA,
DefaultHash: crypto.SHA256,
DefaultCompressionAlgo: packet.CompressionZLIB,
}
return config
}
func (o *OpenPGPSignature) Clone() data.Resource {
return &OpenPGPSignature {
Common: o.Common.Clone(),
KeyRingRef: o.KeyRingRef,
SourceRef: o.SourceRef,
SignatureRef: o.SignatureRef,
}
}
func (o *OpenPGPSignature) Notify(m *machine.EventMessage) {
ctx := context.Background()
switch m.On {
case machine.ENTERSTATEEVENT:
switch m.Dest {
case "start_stat":
if statErr := o.ReadStat(); statErr == nil {
if triggerErr := o.StateMachine().Trigger("exists"); triggerErr == nil {
return
}
} else {
if triggerErr := o.StateMachine().Trigger("notexists"); triggerErr == nil {
return
}
}
case "start_read":
if _,readErr := o.Read(ctx); readErr == nil {
if triggerErr := o.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else {
_ = o.AddError(triggerErr)
}
} else {
_ = o.AddError(readErr)
if o.IsResourceInconsistent() {
if triggerErr := o.StateMachine().Trigger("read-failed"); triggerErr == nil {
panic(readErr)
} else {
_ = o.AddError(triggerErr)
panic(fmt.Errorf("%w - %w", readErr, triggerErr))
}
}
_ = o.AddError(o.StateMachine().Trigger("notexists"))
}
case "start_create":
if createErr := o.Create(ctx); createErr == nil {
if triggerErr := o.StateMachine().Trigger("created"); triggerErr == nil {
return
} else {
_ = o.AddError(triggerErr)
}
} else {
_ = o.AddError(createErr)
if o.IsResourceInconsistent() {
if triggerErr := o.StateMachine().Trigger("create-failed"); triggerErr == nil {
panic(createErr)
} else {
_ = o.AddError(triggerErr)
panic(fmt.Errorf("%w - %w", createErr, triggerErr))
}
}
_ = o.StateMachine().Trigger("notexists")
panic(createErr)
}
case "start_update":
if updateErr := o.Update(ctx); updateErr == nil {
if triggerErr := o.stater.Trigger("updated"); triggerErr == nil {
return
} else {
_ = o.AddError(triggerErr)
}
} else {
_ = o.AddError(updateErr)
if o.IsResourceInconsistent() {
if triggerErr := o.StateMachine().Trigger("update-failed"); triggerErr == nil {
panic(updateErr)
} else {
panic(fmt.Errorf("%w - %w", updateErr, triggerErr))
}
}
_ = o.StateMachine().Trigger("notexists")
panic(updateErr)
}
case "start_delete":
if deleteErr := o.Delete(ctx); deleteErr == nil {
if triggerErr := o.StateMachine().Trigger("deleted"); triggerErr == nil {
return
} else {
o.Common.State = "present"
panic(triggerErr)
}
} else {
_ = o.StateMachine().Trigger("exists")
panic(deleteErr)
}
case "inconsistent":
o.Common.State = "inconsistent"
case "absent":
o.Common.State = "absent"
case "present", "created", "read":
o.Common.State = "present"
}
case machine.EXITSTATEEVENT:
switch m.Dest {
case "start_create":
slog.Info("OpenPGPSignature.Notify - EXITSTATE", "dest", m.Dest, "common.state", o.Common.State)
}
}
}
func (o *OpenPGPSignature) FilePath() string {
return o.Common.Path
}
func (o *OpenPGPSignature) JSON() ([]byte, error) {
return json.Marshal(o)
}
func (o *OpenPGPSignature) Apply() error {
ctx := context.Background()
switch o.Common.State {
case "absent":
return o.Delete(ctx)
case "present":
return o.Create(ctx)
}
return nil
}
func (o *OpenPGPSignature) Load(docData []byte, format codec.Format) (err error) {
err = format.StringDecoder(string(docData)).Decode(o)
return
}
func (o *OpenPGPSignature) LoadReader(r io.ReadCloser, format codec.Format) (err error) {
err = format.Decoder(r).Decode(o)
return
}
func (o *OpenPGPSignature) LoadString(docData string, format codec.Format) (err error) {
err = format.StringDecoder(docData).Decode(o)
return
}
func (o *OpenPGPSignature) LoadDecl(yamlResourceDeclaration string) (err error) {
return o.LoadString(yamlResourceDeclaration, codec.FormatYaml)
}
func (o *OpenPGPSignature) ResolveId(ctx context.Context) string {
if e := o.NormalizePath(); e != nil {
panic(e)
}
return o.Common.Path
}
func (o *OpenPGPSignature) GetContentSourceRef() string {
return string(o.SignatureRef)
}
func (o *OpenPGPSignature) SetContentSourceRef(uri string) {
o.SignatureRef = folio.ResourceReference(uri)
}
func (o *OpenPGPSignature) SignatureRefStat() (info fs.FileInfo, err error) {
err = fmt.Errorf("%w: %s", ErrResourceStateAbsent, o.SignatureRef)
if len(o.SignatureRef) > 0 {
rs, _ := o.ContentReaderStream()
defer rs.Close()
return rs.Stat()
}
return
}
func (o *OpenPGPSignature) Stat() (info fs.FileInfo, err error) {
return o.SignatureRefStat()
}
func (o *OpenPGPSignature) ReadStat() (err error) {
_, err = o.SignatureRefStat()
return err
}
func (o *OpenPGPSignature) Update(ctx context.Context) error {
return o.Create(ctx)
}
func (o *OpenPGPSignature) Delete(ctx context.Context) error {
return os.Remove(o.Common.Path)
}
func (o *OpenPGPSignature) Read(ctx context.Context) ([]byte, error) {
return yaml.Marshal(o)
}
func (o *OpenPGPSignature) ContentReaderStream() (*transport.Reader, error) {
return nil, nil
}