499 lines
14 KiB
Go
499 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"
|
|
"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
|
|
}
|
|
|
|
|