package openpgpops import ( "bytes" "crypto" "errors" "fmt" "os" "time" log "github.com/sirupsen/logrus" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/armor" "golang.org/x/crypto/openpgp/packet" ) const hoursInADay = 24 type OpenPGPRoot struct { Name string SecretKeyRing string Identifier string } func (r *OpenPGPRoot) SignPublicKey(pubKey []byte, algorithm crypto.Hash, days uint16) ([]byte, error) { signingKey, err := r.findSigningKey(r.Identifier) if err != nil { return nil, fmt.Errorf("could not find a signing key matching %s: %w", r.Identifier, err) } pubKeyRing, err := openpgp.ReadKeyRing(bytes.NewReader(pubKey)) if err != nil { return nil, fmt.Errorf("could not read openpgpops keyring: %w", err) } output := bytes.NewBuffer([]byte{}) armorOutput, err := armor.Encode(output, "PGP PUBLIC KEY BLOCK", map[string]string{}) if err != nil { return nil, fmt.Errorf("could not create ASCII armor wrapper for openpgpops output: %w", err) } for _, pe := range pubKeyRing { log.Tracef("found %+v", pe.PrimaryKey.KeyIdString()) for _, i := range pe.Identities { expiry := calculateExpiry(i, days) if !i.SelfSignature.KeyExpired(time.Now()) { sig := &packet.Signature{ SigType: packet.SigTypeGenericCert, PubKeyAlgo: signingKey.PrivateKey.PubKeyAlgo, Hash: algorithm, CreationTime: time.Now(), SigLifetimeSecs: expiry, IssuerKeyId: &signingKey.PrivateKey.KeyId, } if err := sig.SignUserId(i.Name, pe.PrimaryKey, signingKey.PrivateKey, &packet.Config{ DefaultHash: algorithm, }); err != nil { return nil, fmt.Errorf("could not sign identity %s: %w", i.Name, err) } i.Signatures = append(i.Signatures, sig) } } if err = pe.Serialize(armorOutput); err != nil { return nil, fmt.Errorf( "could not write signed public key %s to output: %w", pe.PrimaryKey.KeyIdString(), err, ) } } if err = armorOutput.Close(); err != nil { return nil, fmt.Errorf("could not close output stream: %w", err) } log.Tracef("signed public key\n%s", output.String()) return output.Bytes(), nil } func calculateExpiry(i *openpgp.Identity, days uint16) *uint32 { maxExpiry := time.Second * time.Duration(*i.SelfSignature.KeyLifetimeSecs) calcExpiry := time.Hour * hoursInADay * time.Duration(days) if calcExpiry > maxExpiry { calcExpiry = maxExpiry } expirySeconds := uint32(calcExpiry.Seconds()) return &expirySeconds } func (r *OpenPGPRoot) findSigningKey(identifier string) (*openpgp.Entity, error) { keyring, err := os.Open(r.SecretKeyRing) if err != nil { return nil, fmt.Errorf("could not open secret keyring: %w", err) } defer func() { _ = keyring.Close() }() el, err := openpgp.ReadKeyRing(keyring) if err != nil { return nil, fmt.Errorf("could not read keyring: %w", err) } for _, e := range el { log.Tracef("found %s", e.PrimaryKey.KeyIdString()) for _, i := range e.Identities { if i.UserId.Email == identifier && len(e.Revocations) == 0 && !i.SelfSignature.KeyExpired(time.Now()) { return e, nil } } } return nil, errors.New("no matching key found") } type OpenPGPProfile struct { Name string }