This commit is contained in:
Ben Toews 2017-11-22 08:31:25 -07:00
Коммит a14102fc65
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: E9C423BE17EFEE70
6 изменённых файлов: 588 добавлений и 0 удалений

118
command_sign.go Normal file
Просмотреть файл

@ -0,0 +1,118 @@
package main
import (
"bytes"
"encoding/asn1"
"encoding/pem"
"fmt"
"io"
"os"
"strings"
"github.com/mastahyeti/certstore"
"github.com/mastahyeti/cms"
)
var (
oidEmailAddress = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}
oidCommonName = asn1.ObjectIdentifier{2, 5, 4, 3}
)
func commandSign() int {
store, err := certstore.Open()
if err != nil {
panic(err)
}
defer store.Close()
idents, err := store.Identities()
if err != nil {
panic(err)
}
for _, ident := range idents {
defer ident.Close()
}
userIdent, err := findUserIdentity(idents)
if err != nil {
panic(err)
}
if userIdent == nil {
fmt.Printf("Could not find identity matching specified user-id: %s\n", *localUserOpt)
return 1
}
cert, err := userIdent.Certificate()
if err != nil {
panic(err)
}
signer, err := userIdent.Signer()
if err != nil {
panic(err)
}
dataBuf := new(bytes.Buffer)
if _, err = io.Copy(dataBuf, os.Stdin); err != nil {
panic(err)
}
var der []byte
if *detachSignFlag {
der, err = cms.SignDetached(dataBuf.Bytes(), cert, signer)
} else {
der, err = cms.Sign(dataBuf.Bytes(), cert, signer)
}
if err != nil {
panic(err)
}
// SIG_CREATED
emitSigCreated(cert, *detachSignFlag)
if *armorFlag {
err = pem.Encode(os.Stdout, &pem.Block{
Type: "SIGNED MESSAGE",
Bytes: der,
})
} else {
_, err = os.Stdout.Write(der)
}
if err != nil {
panic(err)
}
return 0
}
// findUserIdentity attempts to find an identity to sign with in the certstore
// by checking available identities against the --local-user argument.
func findUserIdentity(idents []certstore.Identity) (certstore.Identity, error) {
var (
email string
fpr []byte
)
if strings.ContainsRune(*localUserOpt, '@') {
email = normalizeEmail(*localUserOpt)
} else {
fpr = normalizeFingerprint(*localUserOpt)
}
if len(email) == 0 && len(fpr) == 0 {
return nil, fmt.Errorf("bad user-id format: %s", *localUserOpt)
}
for _, ident := range idents {
cert, err := ident.Certificate()
if err != nil {
return nil, err
}
if certHasEmail(cert, email) || certHasFingerprint(cert, fpr) {
return ident, nil
}
}
return nil, nil
}

5
command_verify.go Normal file
Просмотреть файл

@ -0,0 +1,5 @@
package main
func commandVerify() int {
return 0
}

62
main.go Normal file
Просмотреть файл

@ -0,0 +1,62 @@
package main
import (
"fmt"
"os"
"github.com/pborman/getopt/v2"
)
var (
// Action flags
helpFlag = getopt.BoolLong("help", 'h', "print this help message")
signFlag = getopt.BoolLong("sign", 's', "make a signature")
verifyFlag = getopt.BoolLong("verify", 0, "verify a signature")
// Option flags
localUserOpt = getopt.StringLong("local-user", 'u', "", "use USER-ID to sign", "USER-ID")
detachSignFlag = getopt.BoolLong("detach-sign", 'b', "make a detached signature")
armorFlag = getopt.BoolLong("armor", 'a', "create ascii armored output")
statusFdOpt = getopt.IntLong("status-fd", 0, -1, "Write special status strings to the file descriptor n.", "n")
keyFormatOpt = getopt.EnumLong("keyid-format", 0, []string{"short", "0xshort", "long", "0xlong"}, "short", "Select how to display key IDs.", "{short|0xshort|long|0xlong}")
)
func main() {
getopt.HelpColumn = 30
getopt.SetParameters("[files]")
getopt.Parse()
status := 1
if *helpFlag {
if *signFlag || *verifyFlag {
fmt.Println("specify --help, --sign, or --verify")
} else {
getopt.Usage()
status = 0
}
} else if *signFlag {
if *helpFlag || *verifyFlag {
fmt.Println("specify --help, --sign, or --verify")
} else if len(*localUserOpt) == 0 {
fmt.Println("specify a USER-ID to sign with")
} else {
status = commandSign()
}
} else if *verifyFlag {
if *helpFlag || *signFlag {
fmt.Println("specify --help, --sign, or --verify")
} else if len(*localUserOpt) > 0 {
fmt.Println("local-user cannot be specified for verification")
} else if *detachSignFlag {
fmt.Println("detach-sign cannot be specified for verification")
} else if *armorFlag {
fmt.Println("armor cannot be specified for verification")
} else {
status = commandVerify()
}
} else {
fmt.Println("specify --help, --sign, or --verify")
}
os.Exit(status)
}

110
parse_user_id.go Normal file
Просмотреть файл

@ -0,0 +1,110 @@
package main
import "strings"
// The following was copied from the crypto/openpgpg/packet package.
// The original license can be found at https://git.io/vFFwQ
//
// Copyright (c) 2009 The Go Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// The orignal code can be found at https://git.io/vFFwX
//
// parseUserID extracts the name, comment and email from a user id string that
// is formatted as "Full Name (Comment) <email@example.com>".
func parseUserID(id string) (name, comment, email string) {
var n, c, e struct {
start, end int
}
var state int
for offset, rune := range id {
switch state {
case 0:
// Entering name
n.start = offset
state = 1
fallthrough
case 1:
// In name
if rune == '(' {
state = 2
n.end = offset
} else if rune == '<' {
state = 5
n.end = offset
}
case 2:
// Entering comment
c.start = offset
state = 3
fallthrough
case 3:
// In comment
if rune == ')' {
state = 4
c.end = offset
}
case 4:
// Between comment and email
if rune == '<' {
state = 5
}
case 5:
// Entering email
e.start = offset
state = 6
fallthrough
case 6:
// In email
if rune == '>' {
state = 7
e.end = offset
}
default:
// After email
}
}
switch state {
case 1:
// ended in the name
n.end = len(id)
case 3:
// ended in comment
c.end = len(id)
case 6:
// ended in email
e.end = len(id)
}
name = strings.TrimSpace(id[n.start:n.end])
comment = strings.TrimSpace(id[c.start:c.end])
email = strings.TrimSpace(id[e.start:e.end])
return
}

198
status.go Normal file
Просмотреть файл

@ -0,0 +1,198 @@
package main
import (
"crypto"
"crypto/x509"
"fmt"
"os"
"sync"
"time"
"golang.org/x/crypto/openpgp/packet"
"golang.org/x/crypto/openpgp/s2k"
)
// This file implements gnupg's "status protocol". When the --status-fd argument
// is passed, gpg will output machine-readable status updates to that fd.
// Details on the "protocol" can be found at https://git.io/vFFKC
type status string
const (
// SIG_CREATED <type> <pk_algo> <hash_algo> <class> <timestamp> <keyfpr>
// A signature has been created using these parameters.
// Values for type <type> are:
// - D :: detached
// - C :: cleartext
// - S :: standard
// (only the first character should be checked)
//
// <class> are 2 hex digits with the OpenPGP signature class.
//
// Note, that TIMESTAMP may either be a number of seconds since Epoch
// or an ISO 8601 string which can be detected by the presence of the
// letter 'T'.
sSigCreated status = "SIG_CREATED"
// NEWSIG [<signers_uid>]
// Is issued right before a signature verification starts. This is
// useful to define a context for parsing ERROR status messages.
// arguments are currently defined. If SIGNERS_UID is given and is
// not "-" this is the percent escape value of the OpenPGP Signer's
// User ID signature sub-packet.
sNewSig status = "NEWSIG"
// GOODSIG <long_keyid_or_fpr> <username>
// The signature with the keyid is good. For each signature only one
// of the codes GOODSIG, BADSIG, EXPSIG, EXPKEYSIG, REVKEYSIG or
// ERRSIG will be emitted. In the past they were used as a marker
// for a new signature; new code should use the NEWSIG status
// instead. The username is the primary one encoded in UTF-8 and %XX
// escaped. The fingerprint may be used instead of the long keyid if
// it is available. This is the case with CMS and might eventually
// also be available for OpenPGP.
sGoodSig status = "GOODSIG"
// VALIDSIG <args>
//
// The args are:
//
// - <fingerprint_in_hex>
// - <sig_creation_date>
// - <sig-timestamp>
// - <expire-timestamp>
// - <sig-version>
// - <reserved>
// - <pubkey-algo>
// - <hash-algo>
// - <sig-class>
// - [ <primary-key-fpr> ]
//
// This status indicates that the signature is cryptographically
// valid. This is similar to GOODSIG, EXPSIG, EXPKEYSIG, or REVKEYSIG
// (depending on the date and the state of the signature and signing
// key) but has the fingerprint as the argument. Multiple status
// lines (VALIDSIG and the other appropriate *SIG status) are emitted
// for a valid signature. All arguments here are on one long line.
// sig-timestamp is the signature creation time in seconds after the
// epoch. expire-timestamp is the signature expiration time in
// seconds after the epoch (zero means "does not
// expire"). sig-version, pubkey-algo, hash-algo, and sig-class (a
// 2-byte hex value) are all straight from the signature packet.
// PRIMARY-KEY-FPR is the fingerprint of the primary key or identical
// to the first argument. This is useful to get back to the primary
// key without running gpg again for this purpose.
//
// The primary-key-fpr parameter is used for OpenPGP and not
// available for CMS signatures. The sig-version as well as the sig
// class is not defined for CMS and currently set to 0 and 00.
//
// Note, that *-TIMESTAMP may either be a number of seconds since
// Epoch or an ISO 8601 string which can be detected by the presence
// of the letter 'T'.
sValidSig status = "VALIDSIG"
// TRUST_
// These are several similar status codes:
//
// - TRUST_UNDEFINED <error_token>
// - TRUST_NEVER <error_token>
// - TRUST_MARGINAL [0 [<validation_model>]]
// - TRUST_FULLY [0 [<validation_model>]]
// - TRUST_ULTIMATE [0 [<validation_model>]]
//
// For good signatures one of these status lines are emitted to
// indicate the validity of the key used to create the signature.
// The error token values are currently only emitted by gpgsm.
//
// VALIDATION_MODEL describes the algorithm used to check the
// validity of the key. The defaults are the standard Web of Trust
// model for gpg and the standard X.509 model for gpgsm. The
// defined values are
//
// - pgp :: The standard PGP WoT.
// - shell :: The standard X.509 model.
// - chain :: The chain model.
// - steed :: The STEED model.
// - tofu :: The TOFU model
//
// Note that the term =TRUST_= in the status names is used for
// historic reasons; we now speak of validity.
sTrustUndefined status = "TRUST_UNDEFINED"
sTrustNever status = "TRUST_NEVER"
sTrustMarginal status = "TRUST_MARGINAL"
sTrustFully status = "TRUST_FULLY"
sTrustUltimate status = "TRUST_ULTIMATE"
// VERIFICATION_COMPLIANCE_MODE <flags>
// Indicates that the current signature verification operation is in
// compliance with the given set of modes. "flags" is a space
// separated list of numerical flags, see "Field 18 - Compliance
// flags" above.
sVerificationComplianceMode = "VERIFICATION_COMPLIANCE_MODE"
)
var (
setupStatus sync.Once
statusFile *os.File
)
func (s status) emit(format string, args ...interface{}) {
setupStatus.Do(func() {
if *statusFdOpt > 0 {
// TODO: debugging output if this fails
statusFile = os.NewFile(uintptr(*statusFdOpt), "status")
}
})
if statusFile == nil {
return
}
const prefix = "[GNUPG:] "
statusFile.WriteString(prefix)
statusFile.WriteString(string(s))
fmt.Fprintf(statusFile, " "+format+"\n", args...)
}
func emitSigCreated(cert *x509.Certificate, isDetached bool) {
// SIG_CREATED arguments
var (
sigType string
pkAlgo, hashAlgo, sigClass byte
now int64
fpr string
)
if isDetached {
sigType = "D"
} else {
sigType = "S"
}
switch cert.SignatureAlgorithm {
case x509.SHA1WithRSA, x509.SHA256WithRSA, x509.SHA384WithRSA, x509.SHA512WithRSA:
pkAlgo = byte(packet.PubKeyAlgoRSA)
case x509.ECDSAWithSHA1, x509.ECDSAWithSHA256, x509.ECDSAWithSHA384, x509.ECDSAWithSHA512:
pkAlgo = byte(packet.PubKeyAlgoECDSA)
}
switch cert.SignatureAlgorithm {
case x509.SHA1WithRSA, x509.ECDSAWithSHA1:
hashAlgo, _ = s2k.HashToHashId(crypto.SHA1)
case x509.SHA256WithRSA, x509.ECDSAWithSHA256:
hashAlgo, _ = s2k.HashToHashId(crypto.SHA256)
case x509.SHA384WithRSA, x509.ECDSAWithSHA384:
hashAlgo, _ = s2k.HashToHashId(crypto.SHA384)
case x509.SHA512WithRSA, x509.ECDSAWithSHA512:
hashAlgo, _ = s2k.HashToHashId(crypto.SHA512)
}
// gpgsm seems to always use 0x00
sigClass = 0
now = time.Now().Unix()
fpr = certHexFingerprint(cert)
sSigCreated.emit("%s %d %d %02x %d %s", sigType, pkAlgo, hashAlgo, sigClass, now, fpr)
}

95
utils.go Normal file
Просмотреть файл

@ -0,0 +1,95 @@
package main
import (
"bytes"
"crypto/sha1"
"crypto/x509"
"encoding/hex"
"strings"
)
// normalizeFingerprint converts a string fingerprint to hex, removing leading
// "0x", if present.
func normalizeFingerprint(sfpr string) []byte {
if len(sfpr) == 0 {
return nil
}
if strings.HasPrefix(sfpr, "0x") {
sfpr = sfpr[2:]
}
hfpr, err := hex.DecodeString(sfpr)
if err != nil {
return nil
}
return hfpr
}
// certHasFingerprint checks if the given certificate has the given fingerprint.
func certHasFingerprint(cert *x509.Certificate, fpr []byte) bool {
if len(fpr) == 0 {
return false
}
return bytes.HasSuffix(certFingerprint(cert), fpr)
}
// certHexFingerprint calculated the hex SHA1 fingerprint of a certificate.
func certHexFingerprint(cert *x509.Certificate) string {
return hex.EncodeToString(certFingerprint(cert))
}
// certFingerprint calculated the SHA1 fingerprint of a certificate.
func certFingerprint(cert *x509.Certificate) []byte {
if len(cert.Raw) == 0 {
return nil
}
fpr := sha1.Sum(cert.Raw)
return fpr[:]
}
// normalizeEmail attempts to extract an email address from a user-id string.
func normalizeEmail(email string) string {
name, _, email := parseUserID(email)
if len(email) > 0 {
return email
}
if strings.ContainsRune(name, '@') {
return name
}
return ""
}
// certHasEmail checks if a certificate contains the given email address in its
// subject (CN/emailAddress) or SAN fields.
func certHasEmail(cert *x509.Certificate, email string) bool {
if len(email) == 0 {
return false
}
// Check SAN
for _, other := range cert.EmailAddresses {
if other == email {
return true
}
}
// Check CN and emailAddress fields in cert subject.
for _, name := range cert.Subject.Names {
if !name.Type.Equal(oidEmailAddress) && !name.Type.Equal(oidCommonName) {
continue
}
if other, isStr := name.Value.(string); isStr && other == email {
return true
}
}
return false
}