Files
mailgoserver/internal/smime/sign.go
T

100 lines
3.4 KiB
Go
Raw Normal View History

package smime
import (
"crypto"
"crypto/x509"
"errors"
"fmt"
"mime"
"go.mozilla.org/pkcs7"
)
// Sign wraps entity in RFC 8551 multipart/signed: part 1 is the entity's own
// canonical bytes (unmodified — this is what the signature covers), part 2 is a
// detached CMS SignedData over those same bytes.
func Sign(entity Entity, cert *x509.Certificate, key crypto.PrivateKey) (Entity, error) {
content := entity.bytes()
sd, err := pkcs7.NewSignedData(content)
if err != nil {
return Entity{}, fmt.Errorf("smime: sign: %w", err)
}
sd.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
if err := sd.AddSigner(cert, key, pkcs7.SignerInfoConfig{}); err != nil {
return Entity{}, fmt.Errorf("smime: sign: %w", err)
}
sd.Detach()
sigDER, err := sd.Finish()
if err != nil {
return Entity{}, fmt.Errorf("smime: sign: %w", err)
}
boundary := newBoundary()
body := make([]byte, 0, len(content)+len(sigDER)*2)
body = append(body, []byte("--"+boundary+"\r\n")...)
body = append(body, content...)
body = append(body, []byte("\r\n--"+boundary+"\r\n")...)
body = append(body, []byte("Content-Type: application/pkcs7-signature; name=\"smime.p7s\"\r\n")...)
body = append(body, []byte("Content-Transfer-Encoding: base64\r\n")...)
body = append(body, []byte("Content-Disposition: attachment; filename=\"smime.p7s\"\r\n\r\n")...)
body = append(body, []byte(wrapBase64(sigDER))...)
body = append(body, []byte("\r\n--"+boundary+"--\r\n")...)
return Entity{
Headers: []string{
fmt.Sprintf(`Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="%s"`, boundary),
},
Body: body,
}, nil
}
// VerifySigned parses a multipart/signed entity produced by Sign (or any RFC
// 8551-compliant sender), checks the detached signature against the exact original
// bytes of part 1, and returns that inner entity plus the signer's certificate. On a
// signature mismatch it still returns the inner entity — so a tampered or
// unverifiable message can be shown with a warning rather than hidden — alongside a
// non-nil error and a nil signer.
func VerifySigned(entity Entity) (inner Entity, signer *x509.Certificate, err error) {
ct := HeaderValue(entity.Headers, "Content-Type")
mediaType, params, err := mime.ParseMediaType(ct)
if err != nil || mediaType != "multipart/signed" {
return Entity{}, nil, errors.New("smime: not a multipart/signed message")
}
boundary := params["boundary"]
if boundary == "" {
return Entity{}, nil, errors.New("smime: missing multipart boundary")
}
parts, err := splitMultipartRaw(entity.Body, boundary)
if err != nil || len(parts) < 2 {
return Entity{}, nil, errors.New("smime: malformed signed message")
}
signedContent := parts[0]
sigPart, err := parseEntity(parts[1])
if err != nil {
return Entity{}, nil, err
}
sigDER := sigPart.Body
if isBase64CTE(HeaderValue(sigPart.Headers, "Content-Transfer-Encoding")) {
if sigDER, err = decodeBase64(sigPart.Body); err != nil {
return Entity{}, nil, fmt.Errorf("smime: decode signature: %w", err)
}
}
p7, err := pkcs7.Parse(sigDER)
if err != nil {
return Entity{}, nil, fmt.Errorf("smime: parse signature: %w", err)
}
p7.Content = signedContent
inner, perr := parseEntity(signedContent)
if perr != nil {
return Entity{}, nil, perr
}
if err := p7.Verify(); err != nil {
return inner, nil, fmt.Errorf("smime: signature verification failed: %w", err)
}
return inner, p7.GetOnlySigner(), nil
}