package sigcred import ( "bytes" "crypto" "crypto/sha256" "errors" "fmt" "io" "io/ioutil" "os/exec" "path/filepath" "strings" "dehub.dev/src/dehub.git/fs" "dehub.dev/src/dehub.git/yamlutil" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/armor" "golang.org/x/crypto/openpgp/packet" ) // CredentialPGPSignature describes a PGP signature which has been used to sign // a commit. type CredentialPGPSignature struct { PubKeyID string `yaml:"pub_key_id"` PubKeyBody string `yaml:"pub_key_body,omitempty"` Body yamlutil.Blob `yaml:"body"` } // SelfVerify will only work if PubKeyBody is filled in. If so, Body will // attempt to be verified by that public key. func (c *CredentialPGPSignature) SelfVerify(data []byte) error { if c.PubKeyBody == "" { return ErrNotSelfVerifying{ Subject: "PGP signature Credential with no pub_key_body field", } } sig := SignifierPGP{Body: c.PubKeyBody} return sig.Verify(nil, data, CredentialUnion{PGPSignature: c}) } type pgpKey struct { entity *openpgp.Entity } func newPGPPubKey(r io.Reader) (pgpKey, error) { // TODO support non-armored keys as well block, err := armor.Decode(r) if err != nil { return pgpKey{}, fmt.Errorf("could not decode armored PGP public key: %w", err) } entity, err := openpgp.ReadEntity(packet.NewReader(block.Body)) if err != nil { return pgpKey{}, fmt.Errorf("could not read PGP public key: %w", err) } return pgpKey{entity: entity}, nil } func (s pgpKey) Sign(_ fs.FS, data []byte) (CredentialUnion, error) { if s.entity.PrivateKey == nil { return CredentialUnion{}, errors.New("private key not loaded") } h := sha256.New() h.Write(data) var sig packet.Signature sig.Hash = crypto.SHA256 sig.PubKeyAlgo = s.entity.PrimaryKey.PubKeyAlgo if err := sig.Sign(h, s.entity.PrivateKey, nil); err != nil { return CredentialUnion{}, fmt.Errorf("signing data: %w", err) } body := new(bytes.Buffer) if err := sig.Serialize(body); err != nil { return CredentialUnion{}, fmt.Errorf("serializing signature: %w", err) } return CredentialUnion{ PGPSignature: &CredentialPGPSignature{ PubKeyID: s.entity.PrimaryKey.KeyIdString(), Body: body.Bytes(), }, }, nil } func (s pgpKey) Signed(_ fs.FS, cred CredentialUnion) (bool, error) { if cred.PGPSignature == nil { return false, nil } return cred.PGPSignature.PubKeyID == s.entity.PrimaryKey.KeyIdString(), nil } func (s pgpKey) Verify(_ fs.FS, data []byte, cred CredentialUnion) error { credSig := cred.PGPSignature if credSig == nil { return fmt.Errorf("SignifierPGPFile cannot verify %+v", cred) } pkt, err := packet.Read(bytes.NewBuffer(credSig.Body)) if err != nil { return fmt.Errorf("could not read signature packet: %w", err) } sigPkt, ok := pkt.(*packet.Signature) if !ok { return fmt.Errorf("signature bytes were parsed as a %T, not a signature", pkt) } // The gpg process which is invoked during normal signing automatically // hashes whatever is piped to it. The VerifySignature method in the openpgp // package expects you to do it yourself. h := sigPkt.Hash.New() h.Write(data) return s.entity.PrimaryKey.VerifySignature(h, sigPkt) } func (s pgpKey) MarshalBinary() ([]byte, error) { body := new(bytes.Buffer) armorEncoder, err := armor.Encode(body, "PGP PUBLIC KEY", nil) if err != nil { return nil, fmt.Errorf("initializing armor encoder: %w", err) } else if err := s.entity.Serialize(armorEncoder); err != nil { return nil, fmt.Errorf("encoding public key: %w", err) } else if err := armorEncoder.Close(); err != nil { return nil, fmt.Errorf("closing armor encoder: %w", err) } return body.Bytes(), nil } func (s pgpKey) userID() (*packet.UserId, error) { if l := len(s.entity.Identities); l == 0 { return nil, errors.New("pgp key has no identity information") } else if l > 1 { return nil, errors.New("multiple identities on a single pgp key is unsupported") } var identity *openpgp.Identity for _, identity = range s.entity.Identities { break } return identity.UserId, nil } func anonPGPSignifier(pgpKey pgpKey, sig Signifier) (Signifier, error) { keyID := pgpKey.entity.PrimaryKey.KeyIdString() userID, err := pgpKey.userID() if err != nil { return nil, err } pubKeyBody, err := pgpKey.MarshalBinary() if err != nil { return nil, err } return signifierMiddleware{ Signifier: sig, signCallback: func(cred *CredentialUnion) { cred.PGPSignature.PubKeyBody = string(pubKeyBody) cred.AnonID = fmt.Sprintf("%s %q", keyID, userID.Email) }, }, nil } // TestSignifierPGP returns a direct implementation of Signifier which uses a // random private key generated in memory, as well as an armored version of its // public key. // // NOTE that the key returned is very weak, and should only be used for tests. func TestSignifierPGP(name string, anon bool, randReader io.Reader) (Signifier, []byte) { entity, err := openpgp.NewEntity(name, "", name+"@example.com", &packet.Config{ Rand: randReader, RSABits: 512, }) if err != nil { panic(err) } pgpKey := pgpKey{entity: entity} pubKeyBody, err := pgpKey.MarshalBinary() if err != nil { panic(err) } if anon { sigInt, err := anonPGPSignifier(pgpKey, pgpKey) if err != nil { panic(err) } return sigInt, pubKeyBody } return accountSignifier(name, pgpKey), pubKeyBody } // SignifierPGP describes a pgp public key whose corresponding private key will // be used as a signing key. The public key can be described by one of multiple // fields, each being a different method of loading the public key. Only one // field should be set. type SignifierPGP struct { // An armored string encoding of the public key, as exported via // `gpg -a --export ` Body string `yaml:"body,omitempty"` // Path, relative to the root of the repo, of the armored public key file. Path string `yaml:"path,omitempty"` } var _ Signifier = SignifierPGP{} func cmdGPG(stdin []byte, args ...string) ([]byte, error) { args = append([]string{"--openpgp"}, args...) stderr := new(bytes.Buffer) cmd := exec.Command("gpg", args...) cmd.Stdin = bytes.NewBuffer(stdin) cmd.Stderr = stderr out, err := cmd.Output() if err != nil { return nil, fmt.Errorf("calling gpg command (%v): %s", err, stderr.String()) } return out, nil } // LoadSignifierPGP loads a pgp key using the given identifier. The key is // assumed to be stored in the client's keyring already. // // If this is being called for an anonymous user to use, then anon can be set to // true. This will have the effect of setting the PubKeyBody and AnonID of all // produced credentials. func LoadSignifierPGP(keyID string, anon bool) (Signifier, error) { pubKey, err := cmdGPG(nil, "-a", "--export", keyID) if err != nil { return nil, fmt.Errorf("loading public key: %w", err) } else if len(pubKey) == 0 { return nil, fmt.Errorf("no public key found for %q", keyID) } sig := &SignifierPGP{Body: string(pubKey)} if !anon { return sig, nil } pgpKey, err := sig.load(nil) if err != nil { return nil, err } return anonPGPSignifier(pgpKey, sig) } func (s SignifierPGP) load(fs fs.FS) (pgpKey, error) { if s.Body != "" { return newPGPPubKey(strings.NewReader(s.Body)) } path := filepath.Clean(s.Path) fr, err := fs.Open(path) if err != nil { return pgpKey{}, fmt.Errorf("opening PGP public key file at %q: %w", path, err) } defer fr.Close() pubKeyB, err := ioutil.ReadAll(fr) if err != nil { return pgpKey{}, fmt.Errorf("reading PGP public key from file at %q: %w", s.Path, err) } return SignifierPGP{Body: string(pubKeyB)}.load(fs) } // Sign will sign the given arbitrary bytes using the private key corresponding // to the pgp public key embedded in this Signifier. func (s SignifierPGP) Sign(fs fs.FS, data []byte) (CredentialUnion, error) { sigPGP, err := s.load(fs) if err != nil { return CredentialUnion{}, err } keyID := sigPGP.entity.PrimaryKey.KeyIdString() sig, err := cmdGPG(data, "--detach-sign", "--local-user", keyID) if err != nil { return CredentialUnion{}, fmt.Errorf("signing with pgp key: %w", err) } return CredentialUnion{ PGPSignature: &CredentialPGPSignature{ PubKeyID: keyID, Body: sig, }, }, nil } // Signed returns true if the private key corresponding to the pgp public key // embedded in this Signifier was used to produce the given Credential. func (s SignifierPGP) Signed(fs fs.FS, cred CredentialUnion) (bool, error) { sigPGP, err := s.load(fs) if err != nil { return false, err } return sigPGP.Signed(fs, cred) } // Verify asserts that the given signature was produced by this key signing the // given piece of data. func (s SignifierPGP) Verify(fs fs.FS, data []byte, cred CredentialUnion) error { sigPGP, err := s.load(fs) if err != nil { return err } return sigPGP.Verify(fs, data, cred) } // SignifierPGPFile is deprecated and should not be used, use the Path field of // SignifierPGP instead. type SignifierPGPFile struct { Path string `yaml:"path"` }