100 lines
3.4 KiB
Go
100 lines
3.4 KiB
Go
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
|
|
}
|