From 509c11f3a39aeb627cc16dc9ffcad45fc457c5ec Mon Sep 17 00:00:00 2001 From: Roland Shoemaker <roland@golang.org> Date: Mon, 3 Mar 2025 12:31:55 -0800 Subject: [PATCH] crypto,crypto/x509: implement MessageSigner 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 | 5 ++ doc/next/6-stdlib/99-minor/crypto/63405.md | 1 + .../6-stdlib/99-minor/crypto/x509/63405.md | 1 + src/crypto/crypto.go | 32 +++++++ src/crypto/crypto_test.go | 90 +++++++++++++++++++ src/crypto/x509/x509.go | 23 ++--- src/crypto/x509/x509_test.go | 42 +++++++++ 7 files changed, 180 insertions(+), 14 deletions(-) create mode 100644 api/next/63405.txt create mode 100644 doc/next/6-stdlib/99-minor/crypto/63405.md create mode 100644 doc/next/6-stdlib/99-minor/crypto/x509/63405.md create mode 100644 src/crypto/crypto_test.go diff --git a/api/next/63405.txt b/api/next/63405.txt new file mode 100644 index 00000000000..5892ef4adc4 --- /dev/null +++ b/api/next/63405.txt @@ -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 index 00000000000..d16dc5ab00e --- /dev/null +++ b/doc/next/6-stdlib/99-minor/crypto/63405.md @@ -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 index 00000000000..4c3a1750da0 --- /dev/null +++ b/doc/next/6-stdlib/99-minor/crypto/x509/63405.md @@ -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 diff --git a/src/crypto/crypto.go b/src/crypto/crypto.go index 2774a6b6815..f79bedf5815 100644 --- a/src/crypto/crypto.go +++ b/src/crypto/crypto.go @@ -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 index 00000000000..b80fb49c13c --- /dev/null +++ b/src/crypto/crypto_test.go @@ -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) + } +} diff --git a/src/crypto/x509/x509.go b/src/crypto/x509/x509.go index cbcc582a3ff..788b9aca9bd 100644 --- a/src/crypto/x509/x509.go +++ b/src/crypto/x509/x509.go @@ -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. diff --git a/src/crypto/x509/x509_test.go b/src/crypto/x509/x509_test.go index f67f40778b9..7c8972eef4d 100644 --- a/src/crypto/x509/x509_test.go +++ b/src/crypto/x509/x509_test.go @@ -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) + } +} -- GitLab