]> Cypherpunks repositories - gostls13.git/commitdiff
crypto,crypto/x509: implement MessageSigner
authorRoland Shoemaker <roland@golang.org>
Mon, 3 Mar 2025 20:31:55 +0000 (12:31 -0800)
committerGopher Robot <gobot@golang.org>
Thu, 8 May 2025 14:46:12 +0000 (07:46 -0700)
And use it in crypto/x509. This allows people to implement single-shot
signers which do the hashing themselves.

Fixes #63405

Change-Id: I038c2e10f77b050b6136c4c0a5b031cb416f59aa
Reviewed-on: https://go-review.googlesource.com/c/go/+/654375
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>

api/next/63405.txt [new file with mode: 0644]
doc/next/6-stdlib/99-minor/crypto/63405.md [new file with mode: 0644]
doc/next/6-stdlib/99-minor/crypto/x509/63405.md [new file with mode: 0644]
src/crypto/crypto.go
src/crypto/crypto_test.go [new file with mode: 0644]
src/crypto/x509/x509.go
src/crypto/x509/x509_test.go

diff --git a/api/next/63405.txt b/api/next/63405.txt
new file mode 100644 (file)
index 0000000..5892ef4
--- /dev/null
@@ -0,0 +1,5 @@
+pkg crypto, func SignMessage(Signer, io.Reader, []uint8, SignerOpts) ([]uint8, error) #63405
+pkg crypto, type MessageSigner interface { Public, Sign, SignMessage } #63405
+pkg crypto, type MessageSigner interface, Public() PublicKey #63405
+pkg crypto, type MessageSigner interface, Sign(io.Reader, []uint8, SignerOpts) ([]uint8, error) #63405
+pkg crypto, type MessageSigner interface, SignMessage(io.Reader, []uint8, SignerOpts) ([]uint8, error) #63405
diff --git a/doc/next/6-stdlib/99-minor/crypto/63405.md b/doc/next/6-stdlib/99-minor/crypto/63405.md
new file mode 100644 (file)
index 0000000..d16dc5a
--- /dev/null
@@ -0,0 +1 @@
+[MessageSigner] is a new signing interface that can be implemented by signers that wish to hash the message to be signed themselves. A new function is also introduced, [SignMessage] which attempts to update a [Signer] interface to [MessageSigner], using the [MessageSigner.SignMessage] method if successful, and [Signer.Sign] if not. This can be used when code wishes to support both [Signer] and [MessageSigner].
\ No newline at end of file
diff --git a/doc/next/6-stdlib/99-minor/crypto/x509/63405.md b/doc/next/6-stdlib/99-minor/crypto/x509/63405.md
new file mode 100644 (file)
index 0000000..4c3a175
--- /dev/null
@@ -0,0 +1 @@
+[CreateCertificate], [CreateCertificateRequest], and [CreateRevocationList] can now accept a [crypto.MessageSigner] signing interface as well as [crypto.Signer]. This allows these functions to use signers which implement "one-shot" signing interfaces, where hashing is done as part of the signing operation, instead of by the caller.
\ No newline at end of file
index 2774a6b6815fdf0173e646bf7b3b1e92ec59b298..f79bedf58156c929775fccd417b86d0d58019e73 100644 (file)
@@ -198,6 +198,23 @@ type Signer interface {
        Sign(rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error)
 }
 
+// MessageSigner is an interface for an opaque private key that can be used for
+// signing operations where the message is not pre-hashed by the caller.
+// It is a superset of the Signer interface so that it can be passed to APIs
+// which accept Signer, which may try to do an interface upgrade.
+//
+// MessageSigner.SignMessage and MessageSigner.Sign should produce the same
+// result given the same opts. In particular, MessageSigner.SignMessage should
+// only accept a zero opts.HashFunc if the Signer would also accept messages
+// which are not pre-hashed.
+//
+// Implementations which do not provide the pre-hashed Sign API should implement
+// Signer.Sign by always returning an error.
+type MessageSigner interface {
+       Signer
+       SignMessage(rand io.Reader, msg []byte, opts SignerOpts) (signature []byte, err error)
+}
+
 // SignerOpts contains options for signing with a [Signer].
 type SignerOpts interface {
        // HashFunc returns an identifier for the hash function used to produce
@@ -221,3 +238,18 @@ type Decrypter interface {
 }
 
 type DecrypterOpts any
+
+// SignMessage signs msg with signer. If signer implements [MessageSigner],
+// [MessageSigner.SignMessage] is called directly. Otherwise, msg is hashed
+// with opts.HashFunc() and signed with [Signer.Sign].
+func SignMessage(signer Signer, rand io.Reader, msg []byte, opts SignerOpts) (signature []byte, err error) {
+       if ms, ok := signer.(MessageSigner); ok {
+               return ms.SignMessage(rand, msg, opts)
+       }
+       if opts.HashFunc() != 0 {
+               h := opts.HashFunc().New()
+               h.Write(msg)
+               msg = h.Sum(nil)
+       }
+       return signer.Sign(rand, msg, opts)
+}
diff --git a/src/crypto/crypto_test.go b/src/crypto/crypto_test.go
new file mode 100644 (file)
index 0000000..b80fb49
--- /dev/null
@@ -0,0 +1,90 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package crypto_test
+
+import (
+       "crypto"
+       "crypto/rand"
+       "crypto/rsa"
+       "crypto/x509"
+       "encoding/pem"
+       "errors"
+       "io"
+       "strings"
+       "testing"
+)
+
+type messageSignerOnly struct {
+       k *rsa.PrivateKey
+}
+
+func (s *messageSignerOnly) Public() crypto.PublicKey {
+       return s.k.Public()
+}
+
+func (s *messageSignerOnly) Sign(_ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, error) {
+       return nil, errors.New("unimplemented")
+}
+
+func (s *messageSignerOnly) SignMessage(rand io.Reader, msg []byte, opts crypto.SignerOpts) ([]byte, error) {
+       h := opts.HashFunc().New()
+       h.Write(msg)
+       digest := h.Sum(nil)
+       return s.k.Sign(rand, digest, opts)
+}
+
+func TestSignMessage(t *testing.T) {
+       block, _ := pem.Decode([]byte(strings.ReplaceAll(
+               `-----BEGIN RSA TESTING KEY-----
+MIIEowIBAAKCAQEAsPnoGUOnrpiSqt4XynxA+HRP7S+BSObI6qJ7fQAVSPtRkqso
+tWxQYLEYzNEx5ZSHTGypibVsJylvCfuToDTfMul8b/CZjP2Ob0LdpYrNH6l5hvFE
+89FU1nZQF15oVLOpUgA7wGiHuEVawrGfey92UE68mOyUVXGweJIVDdxqdMoPvNNU
+l86BU02vlBiESxOuox+dWmuVV7vfYZ79Toh/LUK43YvJh+rhv4nKuF7iHjVjBd9s
+B6iDjj70HFldzOQ9r8SRI+9NirupPTkF5AKNe6kUhKJ1luB7S27ZkvB3tSTT3P59
+3VVJvnzOjaA1z6Cz+4+eRvcysqhrRgFlwI9TEwIDAQABAoIBAEEYiyDP29vCzx/+
+dS3LqnI5BjUuJhXUnc6AWX/PCgVAO+8A+gZRgvct7PtZb0sM6P9ZcLrweomlGezI
+FrL0/6xQaa8bBr/ve/a8155OgcjFo6fZEw3Dz7ra5fbSiPmu4/b/kvrg+Br1l77J
+aun6uUAs1f5B9wW+vbR7tzbT/mxaUeDiBzKpe15GwcvbJtdIVMa2YErtRjc1/5B2
+BGVXyvlJv0SIlcIEMsHgnAFOp1ZgQ08aDzvilLq8XVMOahAhP1O2A3X8hKdXPyrx
+IVWE9bS9ptTo+eF6eNl+d7htpKGEZHUxinoQpWEBTv+iOoHsVunkEJ3vjLP3lyI/
+fY0NQ1ECgYEA3RBXAjgvIys2gfU3keImF8e/TprLge1I2vbWmV2j6rZCg5r/AS0u
+pii5CvJ5/T5vfJPNgPBy8B/yRDs+6PJO1GmnlhOkG9JAIPkv0RBZvR0PMBtbp6nT
+Y3yo1lwamBVBfY6rc0sLTzosZh2aGoLzrHNMQFMGaauORzBFpY5lU50CgYEAzPHl
+u5DI6Xgep1vr8QvCUuEesCOgJg8Yh1UqVoY/SmQh6MYAv1I9bLGwrb3WW/7kqIoD
+fj0aQV5buVZI2loMomtU9KY5SFIsPV+JuUpy7/+VE01ZQM5FdY8wiYCQiVZYju9X
+Wz5LxMNoz+gT7pwlLCsC4N+R8aoBk404aF1gum8CgYAJ7VTq7Zj4TFV7Soa/T1eE
+k9y8a+kdoYk3BASpCHJ29M5R2KEA7YV9wrBklHTz8VzSTFTbKHEQ5W5csAhoL5Fo
+qoHzFFi3Qx7MHESQb9qHyolHEMNx6QdsHUn7rlEnaTTyrXh3ifQtD6C0yTmFXUIS
+CW9wKApOrnyKJ9nI0HcuZQKBgQCMtoV6e9VGX4AEfpuHvAAnMYQFgeBiYTkBKltQ
+XwozhH63uMMomUmtSG87Sz1TmrXadjAhy8gsG6I0pWaN7QgBuFnzQ/HOkwTm+qKw
+AsrZt4zeXNwsH7QXHEJCFnCmqw9QzEoZTrNtHJHpNboBuVnYcoueZEJrP8OnUG3r
+UjmopwKBgAqB2KYYMUqAOvYcBnEfLDmyZv9BTVNHbR2lKkMYqv5LlvDaBxVfilE0
+2riO4p6BaAdvzXjKeRrGNEKoHNBpOSfYCOM16NjL8hIZB1CaV3WbT5oY+jp7Mzd5
+7d56RZOE+ERK2uz/7JX9VSsM/LbH9pJibd4e8mikDS9ntciqOH/3
+-----END RSA TESTING KEY-----`, "TESTING KEY", "PRIVATE KEY")))
+       k, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
+
+       msg := []byte("hello :)")
+
+       h := crypto.SHA256.New()
+       h.Write(msg)
+       digest := h.Sum(nil)
+
+       sig, err := crypto.SignMessage(k, rand.Reader, msg, &rsa.PSSOptions{Hash: crypto.SHA256})
+       if err != nil {
+               t.Fatalf("SignMessage failed with Signer: %s", err)
+       }
+       if err := rsa.VerifyPSS(&k.PublicKey, crypto.SHA256, digest, sig, nil); err != nil {
+               t.Errorf("VerifyPSS failed for Signer signature: %s", err)
+       }
+
+       sig, err = crypto.SignMessage(&messageSignerOnly{k}, rand.Reader, msg, &rsa.PSSOptions{Hash: crypto.SHA256})
+       if err != nil {
+               t.Fatalf("SignMessage failed with MessageSigner: %s", err)
+       }
+       if err := rsa.VerifyPSS(&k.PublicKey, crypto.SHA256, digest, sig, nil); err != nil {
+               t.Errorf("VerifyPSS failed for MessageSigner signature: %s", err)
+       }
+}
index cbcc582a3ff68afc526a717576cc9738ad7bdd6a..788b9aca9bd13d5716b9fedfb2c931b61545484c 100644 (file)
@@ -1568,13 +1568,7 @@ func signingParamsForKey(key crypto.Signer, sigAlgo SignatureAlgorithm) (Signatu
 }
 
 func signTBS(tbs []byte, key crypto.Signer, sigAlg SignatureAlgorithm, rand io.Reader) ([]byte, error) {
-       signed := tbs
        hashFunc := sigAlg.hashFunc()
-       if hashFunc != 0 {
-               h := hashFunc.New()
-               h.Write(signed)
-               signed = h.Sum(nil)
-       }
 
        var signerOpts crypto.SignerOpts = hashFunc
        if sigAlg.isRSAPSS() {
@@ -1584,7 +1578,7 @@ func signTBS(tbs []byte, key crypto.Signer, sigAlg SignatureAlgorithm, rand io.R
                }
        }
 
-       signature, err := key.Sign(rand, signed, signerOpts)
+       signature, err := crypto.SignMessage(key, rand, tbs, signerOpts)
        if err != nil {
                return nil, err
        }
@@ -1646,7 +1640,7 @@ var emptyASN1Subject = []byte{0x30, 0}
 //
 // The currently supported key types are *rsa.PublicKey, *ecdsa.PublicKey and
 // ed25519.PublicKey. pub must be a supported key type, and priv must be a
-// crypto.Signer with a supported public key.
+// crypto.Signer or crypto.MessageSigner with a supported public key.
 //
 // The AuthorityKeyId will be taken from the SubjectKeyId of parent, if any,
 // unless the resulting certificate is self-signed. Otherwise the value from
@@ -2031,10 +2025,10 @@ func parseCSRExtensions(rawAttributes []asn1.RawValue) ([]pkix.Extension, error)
 //   - Attributes (deprecated)
 //
 // priv is the private key to sign the CSR with, and the corresponding public
-// key will be included in the CSR. It must implement crypto.Signer and its
-// Public() method must return a *rsa.PublicKey or a *ecdsa.PublicKey or a
-// ed25519.PublicKey. (A *rsa.PrivateKey, *ecdsa.PrivateKey or
-// ed25519.PrivateKey satisfies this.)
+// key will be included in the CSR. It must implement crypto.Signer or
+// crypto.MessageSigner and its Public() method must return a *rsa.PublicKey or
+// a *ecdsa.PublicKey or a ed25519.PublicKey. (A *rsa.PrivateKey,
+// *ecdsa.PrivateKey or ed25519.PrivateKey satisfies this.)
 //
 // The returned slice is the certificate request in DER encoding.
 func CreateCertificateRequest(rand io.Reader, template *CertificateRequest, priv any) (csr []byte, err error) {
@@ -2376,8 +2370,9 @@ type tbsCertificateList struct {
 // CreateRevocationList creates a new X.509 v2 [Certificate] Revocation List,
 // according to RFC 5280, based on template.
 //
-// The CRL is signed by priv which should be the private key associated with
-// the public key in the issuer certificate.
+// The CRL is signed by priv which should be a crypto.Signer or
+// crypto.MessageSigner associated with the public key in the issuer
+// certificate.
 //
 // The issuer may not be nil, and the crlSign bit must be set in [KeyUsage] in
 // order to use it as a CRL issuer.
index f67f40778b930bb8b863957a2e486a2747c7c851..7c8972eef4ddcd7b38860957c957dde8aae74e5c 100644 (file)
@@ -22,6 +22,7 @@ import (
        "encoding/gob"
        "encoding/hex"
        "encoding/pem"
+       "errors"
        "fmt"
        "internal/testenv"
        "io"
@@ -4196,3 +4197,44 @@ func TestRejectCriticalSKI(t *testing.T) {
                t.Fatalf("ParseCertificate() unexpected error: %v, want: %s", err, expectedErr)
        }
 }
+
+type messageSigner struct{}
+
+func (ms *messageSigner) Public() crypto.PublicKey { return rsaPrivateKey.Public() }
+
+func (ms *messageSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
+       return nil, errors.New("unimplemented")
+}
+
+func (ms *messageSigner) SignMessage(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) {
+       if _, ok := opts.(*rsa.PSSOptions); ok {
+               return nil, errors.New("PSSOptions passed instead of hash")
+       }
+       h := opts.HashFunc().New()
+       h.Write(msg)
+       tbs := h.Sum(nil)
+       return rsa.SignPKCS1v15(rand, rsaPrivateKey, opts.HashFunc(), tbs)
+}
+
+func TestMessageSigner(t *testing.T) {
+       template := Certificate{
+               SignatureAlgorithm:    SHA256WithRSA,
+               SerialNumber:          big.NewInt(1),
+               Subject:               pkix.Name{CommonName: "Cert"},
+               NotBefore:             time.Unix(1000, 0),
+               NotAfter:              time.Unix(100000, 0),
+               BasicConstraintsValid: true,
+               IsCA:                  true,
+       }
+       certDER, err := CreateCertificate(rand.Reader, &template, &template, rsaPrivateKey.Public(), &messageSigner{})
+       if err != nil {
+               t.Fatalf("CreateCertificate failed: %s", err)
+       }
+       cert, err := ParseCertificate(certDER)
+       if err != nil {
+               t.Fatalf("ParseCertificate failed: %s", err)
+       }
+       if err := cert.CheckSignatureFrom(cert); err != nil {
+               t.Fatalf("CheckSignatureFrom failed: %s", err)
+       }
+}