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 }