Skip to content

Commit 7e7de0e

Browse files
authored
[protoutil] Introduce HashTLSCertificate() (#93)
#### Type of change - New feature #### Description - Introduce `protoutil.HashTLSCertificate()` to produce the expected hash for `protoutil.CreateSignedEnvelopeWithTLSBinding()` Signed-off-by: Liran Funaro <liran.funaro@gmail.com>
1 parent 12a17e4 commit 7e7de0e

3 files changed

Lines changed: 132 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ TESTS*.xml
2525
.tox/
2626
.vagrant/
2727
.vscode
28+
.bob
2829
*coverage.profile

protoutil/txutils.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import (
1010
"bytes"
1111
"crypto/sha256"
1212
b64 "encoding/base64"
13+
"encoding/pem"
1314

15+
"github.com/cockroachdb/errors"
1416
"github.com/hyperledger/fabric-protos-go-apiv2/common"
1517
"github.com/hyperledger/fabric-protos-go-apiv2/peer"
16-
"github.com/pkg/errors"
1718
"google.golang.org/protobuf/proto"
1819

1920
"github.com/hyperledger/fabric-x-common/protoutil/identity"
@@ -59,6 +60,46 @@ func GetEnvelopeFromBlock(data []byte) (*common.Envelope, error) {
5960
return env, nil
6061
}
6162

63+
// HashTLSCertificate computes the SHA256 hash of a PEM-encoded TLS certificate.
64+
// The certificate must be provided in PEM format with block type "CERTIFICATE".
65+
// This hash can be used with CreateSignedEnvelopeWithTLSBinding to bind a transaction
66+
// to a specific TLS certificate.
67+
//
68+
// Parameters:
69+
// - certPEMBlock: PEM-encoded TLS certificate bytes.
70+
//
71+
// Returns:
72+
// - The SHA256 hash of the certificate DER bytes.
73+
// - An error if the input is not a valid PEM-encoded certificate.
74+
//
75+
// Example usage:
76+
//
77+
// tlsCertHash, err := protoutil.HashTLSCertificate(certPEMBlock)
78+
// if err != nil {
79+
// return err
80+
// }
81+
// envelope, err := protoutil.CreateSignedEnvelopeWithTLSBinding(
82+
// txType, channelID, signer, dataMsg, msgVersion, epoch, tlsCertHash)
83+
func HashTLSCertificate(certPEMBlock []byte) ([]byte, error) {
84+
var tlsCertBytes []byte
85+
for len(tlsCertBytes) == 0 {
86+
var certDERBlock *pem.Block
87+
certDERBlock, certPEMBlock = pem.Decode(certPEMBlock)
88+
if certDERBlock == nil {
89+
break
90+
}
91+
if certDERBlock.Type == "CERTIFICATE" {
92+
tlsCertBytes = certDERBlock.Bytes
93+
}
94+
}
95+
if len(tlsCertBytes) == 0 {
96+
return nil, errors.New("failed to find any PEM data in certificate input")
97+
}
98+
99+
digest := sha256.Sum256(tlsCertBytes)
100+
return digest[:], nil
101+
}
102+
62103
// CreateSignedEnvelope creates a signed envelope with
63104
// cert in the signature header.
64105
func CreateSignedEnvelope( //nolint:revive // argument-limit; max 4 but got 6

protoutil/txutils_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,28 @@ SPDX-License-Identifier: Apache-2.0
77
package protoutil_test
88

99
import (
10+
"crypto/ecdsa"
11+
"crypto/elliptic"
12+
"crypto/rand"
13+
"crypto/sha256"
14+
"crypto/tls"
15+
"crypto/x509"
1016
"encoding/hex"
17+
"encoding/pem"
1118
"errors"
1219
"strconv"
1320
"strings"
1421
"testing"
1522

1623
cb "github.com/hyperledger/fabric-protos-go-apiv2/common"
1724
pb "github.com/hyperledger/fabric-protos-go-apiv2/peer"
25+
"google.golang.org/grpc/credentials"
26+
"google.golang.org/grpc/peer"
1827
"google.golang.org/protobuf/proto"
1928

2029
"github.com/stretchr/testify/require"
2130

31+
"github.com/hyperledger/fabric-x-common/common/util"
2232
"github.com/hyperledger/fabric-x-common/protoutil"
2333
"github.com/hyperledger/fabric-x-common/protoutil/identity/mocks"
2434
)
@@ -120,6 +130,85 @@ func TestGetPayloads(t *testing.T) {
120130
t.Logf("error6 [%s]", err)
121131
}
122132

133+
func TestHashTLSCertificate(t *testing.T) {
134+
t.Parallel()
135+
136+
t.Run("PEM encoded certificate", func(t *testing.T) {
137+
t.Parallel()
138+
_, certPEM := generateTestCertPEM(t, "CERTIFICATE")
139+
140+
hash, err := protoutil.HashTLSCertificate(certPEM)
141+
require.NoError(t, err)
142+
require.NotNil(t, hash)
143+
require.Len(t, hash, 32) // SHA256 produces 32 bytes
144+
145+
// The hash should be deterministic.
146+
hash2, err := protoutil.HashTLSCertificate(certPEM)
147+
require.NoError(t, err)
148+
require.Equal(t, hash, hash2)
149+
})
150+
151+
t.Run("Hash matches server-side extraction", func(t *testing.T) {
152+
t.Parallel()
153+
certDER, certPEM := generateTestCertPEM(t, "CERTIFICATE")
154+
155+
// Simulate server-side: create a gRPC context with TLS peer info
156+
cert, err := x509.ParseCertificate(certDER)
157+
require.NoError(t, err)
158+
ctx := peer.NewContext(t.Context(), &peer.Peer{
159+
AuthInfo: credentials.TLSInfo{
160+
State: tls.ConnectionState{PeerCertificates: []*x509.Certificate{cert}},
161+
},
162+
})
163+
// Use ExtractRawCertificateFromContext (server-side).
164+
rawCert := util.ExtractRawCertificateFromContext(ctx)
165+
require.NotNil(t, rawCert)
166+
167+
// Hash the raw certificate (server-side).
168+
serverHash := sha256.Sum256(rawCert)
169+
170+
// Hash using HashTLSCertificate (client-side).
171+
clientHash, err := protoutil.HashTLSCertificate(certPEM)
172+
require.NoError(t, err)
173+
174+
// Verify both hashes match.
175+
require.Equal(t, serverHash[:], clientHash, "Client-side hash should match server-side hash")
176+
})
177+
178+
_, wrongCertPEMBlockType := generateTestCertPEM(t, "EC PRIVATE KEY")
179+
for _, tc := range []struct {
180+
name string
181+
cert []byte
182+
}{
183+
{name: "Empty certificate", cert: nil},
184+
{name: "Invalid certificate", cert: []byte("not a valid PEM certificate")},
185+
{name: "Wrong PEM block type", cert: wrongCertPEMBlockType},
186+
} {
187+
t.Run(tc.name, func(t *testing.T) {
188+
t.Parallel()
189+
_, err := protoutil.HashTLSCertificate(tc.cert)
190+
require.ErrorContains(t, err, "failed to find any PEM data in certificate input")
191+
})
192+
}
193+
}
194+
195+
func generateTestCertPEM(t *testing.T, blockType string) (certDER, certPEM []byte) {
196+
t.Helper()
197+
198+
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
199+
require.NoError(t, err)
200+
201+
template := &x509.Certificate{}
202+
certDER, err = x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
203+
require.NoError(t, err)
204+
require.NotNil(t, certDER)
205+
206+
certPEM = pem.EncodeToMemory(&pem.Block{Type: blockType, Bytes: certDER})
207+
require.NotNil(t, certPEM)
208+
209+
return certDER, certPEM
210+
}
211+
123212
func TestDeduplicateEndorsements(t *testing.T) {
124213
signID := &mocks.SignerSerializer{}
125214
signID.SerializeReturns([]byte("signer"), nil)

0 commit comments

Comments
 (0)