// Copyright 2024 Matthew Rich . 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 ( ErrSignatureFailedDecodingSignature error = errors.New("Failed decoding signature") ErrSignatureWriterFailed error = errors.New("Failed creating signature writer") ErrArmoredWriterFailed error = errors.New("Failed to create armored writer") ErrSignatureVerificationUnknownEntity error = errors.New("Signature uses unknown entity") ErrSignatureMissing error = errors.New("Signature value undefined") ) 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"` Signed string `json:"signed,omitempty" yaml:"signed,omitempty"` KeyRingRef folio.Ref `json:"keyringref,omitempty" yaml:"keyringref,omitempty"` SourceRef folio.Ref `json:"soureref,omitempty" yaml:"sourceref,omitempty"` SignatureRef folio.Ref `json:"signatureref,omitempty" yaml:"signatureref,omitempty"` signatureBlock *armor.Block 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 { parsedSourceUri := o.SourceRef.Uri.Parse().URL() return fmt.Sprintf("%s://%s", o.Type(), parsedSourceUri.Host + parsedSourceUri.RequestURI()) } func (o *OpenPGPSignature) DecryptPrivateKey(entity *openpgp.Entity) error { if o.config != nil { passphraseConfig, _ := o.config.GetValue("passphrase") passphrase := []byte(passphraseConfig.(string)) if len(passphrase) > 0 { slog.Info("OpenPGPSignature.DecryptPrivateKey", "passphrase", passphrase, "entity", entity) if decryptErr := entity.PrivateKey.Decrypt(passphrase); decryptErr != nil { return fmt.Errorf("%w private key: %w", ErrOpenPGPDecryptionFailure, decryptErr) } for _, subkey := range entity.Subkeys { if decryptErr := subkey.PrivateKey.Encrypt(passphrase); decryptErr != nil { return fmt.Errorf("%w subkey (private key): %w", ErrOpenPGPDecryptionFailure, decryptErr) } } } } return nil } func (o *OpenPGPSignature) SigningEntity() (err error) { if len(o.entityList) < 1 { slog.Info("OpenPGPSignature.SigningEntity() - Loading KeyRing", "keyring", o.KeyRingRef) 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) slog.Info("OpenPGPSignature.SigningEntity()", "entities", o.entityList[0]) } for i := range o.entityList { if decryptErr := o.DecryptPrivateKey(o.entityList[i]); decryptErr != nil { err = decryptErr } } } return } func (o *OpenPGPSignature) Sign(message io.Reader, w io.Writer) (err error) { entity := o.entityList[0] if err = openpgp.DetachSign(w, entity, message, o.Config()); err != nil { err = fmt.Errorf("%w: %w", ErrSignatureWriterFailed, err) } return } func (o *OpenPGPSignature) Verify(message io.Reader) (err error) { if len(o.Signature) < 1 || o.signatureBlock == nil { return fmt.Errorf("%w - %d", ErrSignatureMissing, len(o.Signature)) } slog.Info("OpenPGPSignature.Verify()", "signature", o.Signature, "block", o.signatureBlock) var entity *openpgp.Entity entity, err = openpgp.CheckDetachedSignature(o.entityList, message, o.signatureBlock.Body, o.Config()) if entity == nil { slog.Info("OpenPGPSignature.Verify() check signature failed", "signature", o.signatureBlock, "err", err) return fmt.Errorf("%w: %w", ErrSignatureVerificationUnknownEntity, err) } return } func (o *OpenPGPSignature) Create(ctx context.Context) (err error) { if err = o.SigningEntity(); err == nil { var sourceReadStream io.ReadCloser switch o.SourceRef.RefType { case folio.ReferenceTypeDocument: //sourceReadStream, err = o.SourceRef.Lookup(folio.DocumentRegistry.UriMap).ContentReaderStream() sourceReadStream, err = o.SourceRef.Reader() srcData, _ := io.ReadAll(sourceReadStream) sourceReadStream.Close() o.Signed = string(srcData) sourceReadStream, err = o.SourceRef.Reader() default: sourceReadStream, err = o.SourceRef.Lookup(o.Resources).ContentReaderStream() } defer sourceReadStream.Close() 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) } err = o.Sign(sourceReadStream, armoredWriter) armoredWriter.Close() } 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 { slog.Info("OpenPGPSignature.Notify()", "dest", "start_read", "err", readErr) _ = o.AddError(readErr) if o.IsResourceInconsistent() { slog.Info("OpenPGPSignature.Notify()", "dest", "read-failed", "machine", o.StateMachine()) 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 o.SignatureRef.String() } func (o *OpenPGPSignature) SetContentSourceRef(uri string) { o.SignatureRef.Uri = folio.URI(uri) } func (o *OpenPGPSignature) SignatureRefStat() (info fs.FileInfo, err error) { err = fmt.Errorf("%w: %s", ErrResourceStateAbsent, o.SignatureRef.Uri) if len(o.SignatureRef.Uri) > 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) SourceRefStat() (info fs.FileInfo, err error) { err = fmt.Errorf("%w: %s", ErrResourceStateAbsent, o.SourceRef.Uri) if ! o.SourceRef.IsEmpty() { rs, _ := o.SourceRef.ContentReaderStream() defer rs.Close() return rs.Stat() } return } 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) readSignatureRef() (err error) { // signatureref // XXX which takes precedence: the value of Signature from yaml or the value of Signature loaded from SignatureRef? if (! o.SignatureRef.IsEmpty()) && o.SignatureRef.Exists() { signatureReader, _ := o.SignatureRef.Lookup(o.Resources).ContentReaderStream() defer signatureReader.Close() SignatureData, readErr := io.ReadAll(signatureReader) if readErr != nil { return readErr } o.Signature = string(SignatureData) } return nil } func (o *OpenPGPSignature) DecodeSignatureBlock() (err error) { slog.Info("OpenPGPSignature.DecodeSignatureBlock()", "signature", o.Signature) if o.signatureBlock, err = armor.Decode(bytes.NewReader([]byte(o.Signature))); o.signatureBlock == nil || err != nil { err = ErrSignatureFailedDecodingSignature } return } // Read // - signature(string/ref) content // - keyringref -> loads signing entity // - sourceref to generate a signature (create) func (o *OpenPGPSignature) Read(ctx context.Context) (yamlData []byte, err error) { if len(o.Signature) < 1 { if err = o.readSignatureRef(); err != nil { panic(err) } } if err = o.DecodeSignatureBlock(); err != nil { panic(err) } slog.Info("OpenPGPSignature.Read() - decodesignatureblock", "sourceref", o.SourceRef, "entityList", o.entityList, "signatureblock", o.signatureBlock) // sourceref if _, sourceRefStatErr := o.SourceRefStat(); sourceRefStatErr != nil { return nil, sourceRefStatErr } slog.Info("OpenPGPSignature.Read() - sourceref", "sourceref", o.SourceRef, "entityList", o.entityList, "signatureblock", o.signatureBlock) if err = o.SigningEntity(); err != nil { return nil, err } slog.Info("OpenPGPSignature.Read() - reading", "sourceref", o.SourceRef, "entityList", o.entityList, "signatureblock", o.signatureBlock) slog.Info("OpenPGPSignature.Read() - reading sourceref", "openpgp-signature", o) if sourceRefReader, sourceRefReaderErr := o.SourceRef.Reader(); sourceRefReaderErr != nil { return nil, sourceRefReaderErr } else { defer sourceRefReader.Close() if err = o.Verify(sourceRefReader); err != nil { return nil, err } } o.Common.State = "present" slog.Info("OpenPGPSignature.Read()", "openpgp-signature", o) return yaml.Marshal(o) } func (o *OpenPGPSignature) ContentReaderStream() (*transport.Reader, error) { return nil, nil }