зеркало из https://github.com/github/smimesign.git
get signing working
This commit is contained in:
Коммит
a14102fc65
|
@ -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
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package main
|
||||
|
||||
func commandVerify() int {
|
||||
return 0
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
Загрузка…
Ссылка в новой задаче