Skip to content

Commit dc45e23

Browse files
adrianoselaJoTurk
authored andcommitted
Fix OpenSSL Interop for RSA SignatureHashAlgos
1 parent 61762de commit dc45e23

5 files changed

Lines changed: 241 additions & 25 deletions

File tree

e2e/e2e_openssl_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,3 +345,21 @@ func TestPionOpenSSLE2ESimpleECDSAClientCert(t *testing.T) {
345345
testPionE2ESimpleECDSAClientCert(t, serverPion, clientOpenSSL)
346346
})
347347
}
348+
349+
func TestPionOpenSSLE2ESimpleRSA(t *testing.T) {
350+
t.Run("OpenSSLServer", func(t *testing.T) {
351+
testPionE2ESimpleRSA(t, serverOpenSSL, clientPion)
352+
})
353+
t.Run("OpenSSLClient", func(t *testing.T) {
354+
testPionE2ESimpleRSA(t, serverPion, clientOpenSSL)
355+
})
356+
}
357+
358+
func TestPionOpenSSLE2ESimpleRSAClientCert(t *testing.T) {
359+
t.Run("OpenSSLServer", func(t *testing.T) {
360+
testPionE2ESimpleRSAClientCert(t, serverOpenSSL, clientPion)
361+
})
362+
t.Run("OpenSSLClient", func(t *testing.T) {
363+
testPionE2ESimpleRSAClientCert(t, serverPion, clientOpenSSL)
364+
})
365+
}

e2e/e2e_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ func withConnectionIDGenerator(g func() []byte) dtlsTestOpts {
335335
// - Assert that you can send messages both ways
336336
// - Assert that Close() on both ends work
337337
// - Assert that no Goroutines are leaked
338+
//
339+
//nolint:dupl
338340
func testPionE2ESimple(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
339341
t.Helper()
340342
lim := test.TimeOut(time.Second * 30)
@@ -382,6 +384,56 @@ func testPionE2ESimple(t *testing.T, server, client func(*comm), opts ...dtlsTes
382384
}
383385
}
384386

387+
//nolint:dupl
388+
func testPionE2ESimpleRSA(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
389+
t.Helper()
390+
lim := test.TimeOut(time.Second * 30)
391+
defer lim.Stop()
392+
393+
report := test.CheckRoutines(t)
394+
defer report()
395+
396+
for _, cipherSuite := range []dtls.CipherSuiteID{
397+
dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
398+
dtls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
399+
dtls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
400+
} {
401+
cipherSuite := cipherSuite
402+
t.Run(cipherSuite.String(), func(t *testing.T) {
403+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
404+
defer cancel()
405+
406+
priv, err := rsa.GenerateKey(rand.Reader, 2048)
407+
assert.NoError(t, err)
408+
cert, err := selfsign.SelfSign(priv)
409+
assert.NoError(t, err)
410+
411+
clientOpts := []dtls.ClientOption{
412+
dtls.WithCertificates(cert),
413+
dtls.WithCipherSuites(cipherSuite),
414+
dtls.WithInsecureSkipVerify(true),
415+
}
416+
serverOpts := []dtls.ServerOption{
417+
dtls.WithCertificates(cert),
418+
dtls.WithCipherSuites(cipherSuite),
419+
dtls.WithInsecureSkipVerify(true),
420+
}
421+
for _, o := range opts {
422+
clientOpts = append(clientOpts, o.clientOpts...)
423+
serverOpts = append(serverOpts, o.serverOpts...)
424+
}
425+
serverPort := randomPort(t)
426+
comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
427+
comm.setOpenSSLInfo(
428+
[]dtls.CipherSuiteID{cipherSuite}, []dtls.CipherSuiteID{cipherSuite},
429+
[]tls.Certificate{cert}, []tls.Certificate{cert},
430+
nil, nil, nil, nil, true)
431+
defer comm.cleanup(t)
432+
comm.assert(t)
433+
})
434+
}
435+
}
436+
385437
func testPionE2ESimplePSK(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
386438
t.Helper()
387439

@@ -812,6 +864,10 @@ func TestPionE2ESimple(t *testing.T) {
812864
testPionE2ESimple(t, serverPion, clientPion)
813865
}
814866

867+
func TestPionE2ESimpleRSA(t *testing.T) {
868+
testPionE2ESimpleRSA(t, serverPion, clientPion)
869+
}
870+
815871
func TestPionE2ESimplePSK(t *testing.T) {
816872
testPionE2ESimplePSK(t, serverPion, clientPion)
817873
}

pkg/crypto/signaturehash/signaturehash.go

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,15 @@ type Algorithm struct {
2626
Signature signature.Algorithm
2727
}
2828

29-
// Algorithms are all the known SignatureHash Algorithms.
29+
// Algorithms returns signature algorithms compatible with DTLS 1.2 / TLS 1.2.
30+
// This excludes TLS 1.3-specific schemes like RSA-PSS to ensure compatibility
31+
// with implementations like OpenSSL that don't recognize TLS 1.3 signature
32+
// algorithm IDs in DTLS 1.2 handshakes.
3033
//
31-
// IMPORTANT: order in this slice determines priority used
32-
// by SelectSignatureScheme and SelectSignatureScheme13.
34+
// IMPORTANT: order in this slice determines priority used by SelectSignatureScheme.
3335
//
34-
// Order follows industry standard preference (ECDSA-first) as used
35-
// by: OpenSSL, BoringSSL, Firefox, Chrome, and other major TLS 1.3
36-
// implementations. This prioritizes elliptic curve crypto for better
37-
// performance (smaller keys, faster computations, less bandwidth).
38-
//
39-
// Note that this is different than Go's TLS implementation order of
40-
// preference.
36+
// Order follows industry standard preference (ECDSA-first) as used by OpenSSL,
37+
// BoringSSL, Firefox, Chrome, and other major TLS implementations.
4138
func Algorithms() []Algorithm {
4239
return []Algorithm{
4340
// ECDSA schemes (modern, efficient - industry standard preference)
@@ -48,21 +45,6 @@ func Algorithms() []Algorithm {
4845
// Ed25519
4946
{hash.Ed25519, signature.Ed25519},
5047

51-
// RSA-PSS RSAE schemes (DTLS 1.3 compatible with standard RSA certs)
52-
// Note: We only offer RSA_PSS_RSAE variants (0x0804-0x0806), not RSA_PSS_PSS
53-
// (0x0809-0x080b). RSA-PSS certificates with OID id-RSASSA-PSS are virtually
54-
// unused in the real world and are not allowed by the CA/Browser Forum Baseline
55-
// Requirements for WebPKI. We avoid unnecessary complexity for certificates that
56-
// don't exist in practice, following the pragmatic approach of Go's crypto/tls
57-
// and BoringSSL: target real-world WebPKI use cases rather than RFC completeness.
58-
// RSA_PSS_PSS schemes are parsed for wire-format compatibility but never negotiated.
59-
{hash.SHA256, signature.RSA_PSS_RSAE_SHA256},
60-
{hash.SHA384, signature.RSA_PSS_RSAE_SHA384},
61-
{hash.SHA512, signature.RSA_PSS_RSAE_SHA512},
62-
// {hash.SHA256, signature.RSA_PSS_PSS_SHA256},
63-
// {hash.SHA384, signature.RSA_PSS_PSS_SHA384},
64-
// {hash.SHA512, signature.RSA_PSS_PSS_SHA512},
65-
6648
// RSA PKCS#1 v1.5 schemes (legacy, DTLS 1.2)
6749
{hash.SHA256, signature.RSA},
6850
{hash.SHA384, signature.RSA},
@@ -98,6 +80,10 @@ func (a *Algorithm) isCompatible(signer crypto.Signer) bool {
9880
// ParseSignatureSchemes translates []tls.SignatureScheme to []signatureHashAlgorithm.
9981
// It returns default signature scheme list if no SignatureScheme is passed.
10082
// This function handles both TLS 1.2 byte-split encoding and TLS 1.3 PSS full uint16 schemes.
83+
//
84+
// For DTLS 1.2 / TLS 1.2, this returns Algorithms() which excludes TLS 1.3-specific
85+
// schemes like RSA-PSS for compatibility with implementations like OpenSSL.
86+
// When DTLS 1.3 is implemented, use Algorithms13() or create ParseSignatureSchemes13().
10187
func ParseSignatureSchemes(sigs []tls.SignatureScheme, insecureHashes bool) ([]Algorithm, error) {
10288
if len(sigs) == 0 {
10389
return Algorithms(), nil
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
package signaturehash
5+
6+
import (
7+
"github.com/pion/dtls/v3/pkg/crypto/hash"
8+
"github.com/pion/dtls/v3/pkg/crypto/signature"
9+
)
10+
11+
// Algorithms13 returns signature algorithms compatible with DTLS 1.3. This.
12+
// includes DTLS 1.3-specific schemes like RSA-PSS in addition to DTLS 1.2 schemes.
13+
//
14+
// IMPORTANT: order in this slice determines priority used by SelectSignatureScheme13.
15+
//
16+
// Order follows industry standard preference (ECDSA-first) as used by OpenSSL,
17+
// BoringSSL, Firefox, Chrome, and other major TLS 1.3 implementations.
18+
func Algorithms13() []Algorithm {
19+
return []Algorithm{
20+
// ECDSA schemes (modern, efficient - industry standard preference)
21+
{hash.SHA256, signature.ECDSA},
22+
{hash.SHA384, signature.ECDSA},
23+
{hash.SHA512, signature.ECDSA},
24+
25+
// Ed25519
26+
{hash.Ed25519, signature.Ed25519},
27+
28+
// RSA-PSS RSAE schemes (TLS 1.3 / DTLS 1.3 compatible with standard RSA certs)
29+
// Note: We only offer RSA_PSS_RSAE variants (0x0804-0x0806), not RSA_PSS_PSS
30+
// (0x0809-0x080b). RSA-PSS certificates with OID id-RSASSA-PSS are virtually
31+
// unused in the real world and are not allowed by the CA/Browser Forum Baseline
32+
// Requirements for WebPKI. We avoid unnecessary complexity for certificates that
33+
// don't exist in practice, following the pragmatic approach of Go's crypto/tls
34+
// and BoringSSL: target real-world WebPKI use cases rather than RFC completeness.
35+
// RSA_PSS_PSS schemes are parsed for wire-format compatibility but never negotiated.
36+
{hash.SHA256, signature.RSA_PSS_RSAE_SHA256},
37+
{hash.SHA384, signature.RSA_PSS_RSAE_SHA384},
38+
{hash.SHA512, signature.RSA_PSS_RSAE_SHA512},
39+
// {hash.SHA256, signature.RSA_PSS_PSS_SHA256},
40+
// {hash.SHA384, signature.RSA_PSS_PSS_SHA384},
41+
// {hash.SHA512, signature.RSA_PSS_PSS_SHA512},
42+
43+
// RSA PKCS#1 v1.5 schemes (backward compatibility with DTLS 1.2)
44+
{hash.SHA256, signature.RSA},
45+
{hash.SHA384, signature.RSA},
46+
{hash.SHA512, signature.RSA},
47+
}
48+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
package signaturehash
5+
6+
import (
7+
"testing"
8+
9+
"github.com/pion/dtls/v3/pkg/crypto/hash"
10+
"github.com/pion/dtls/v3/pkg/crypto/signature"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestAlgorithms13(t *testing.T) {
15+
algos := Algorithms13()
16+
17+
// Verify we got expected number of algorithms
18+
// ECDSA (3) + Ed25519 (1) + RSA-PSS (3) + RSA PKCS#1 (3) = 10
19+
assert.Len(t, algos, 10, "Algorithms13 should return 10 signature schemes")
20+
21+
// Verify ECDSA schemes come first (industry standard preference)
22+
assert.Equal(t, Algorithm{hash.SHA256, signature.ECDSA}, algos[0])
23+
assert.Equal(t, Algorithm{hash.SHA384, signature.ECDSA}, algos[1])
24+
assert.Equal(t, Algorithm{hash.SHA512, signature.ECDSA}, algos[2])
25+
26+
// Verify Ed25519
27+
assert.Equal(t, Algorithm{hash.Ed25519, signature.Ed25519}, algos[3])
28+
29+
// Verify RSA-PSS schemes (TLS 1.3 preference for RSA)
30+
assert.Equal(t, Algorithm{hash.SHA256, signature.RSA_PSS_RSAE_SHA256}, algos[4])
31+
assert.Equal(t, Algorithm{hash.SHA384, signature.RSA_PSS_RSAE_SHA384}, algos[5])
32+
assert.Equal(t, Algorithm{hash.SHA512, signature.RSA_PSS_RSAE_SHA512}, algos[6])
33+
34+
// Verify RSA PKCS#1 v1.5 schemes come last (TLS 1.2 compatibility)
35+
assert.Equal(t, Algorithm{hash.SHA256, signature.RSA}, algos[7])
36+
assert.Equal(t, Algorithm{hash.SHA384, signature.RSA}, algos[8])
37+
assert.Equal(t, Algorithm{hash.SHA512, signature.RSA}, algos[9])
38+
}
39+
40+
func TestAlgorithms_DTLS12_ExcludesRSAPSS(t *testing.T) {
41+
algos := Algorithms()
42+
43+
// Verify DTLS 1.2 algorithms exclude RSA-PSS schemes
44+
for _, algo := range algos {
45+
assert.False(t, algo.Signature.IsPSS(),
46+
"Algorithms() for DTLS 1.2 should not include RSA-PSS schemes (found %v)", algo)
47+
}
48+
49+
// Verify we still have RSA PKCS#1 v1.5
50+
hasRSA := false
51+
for _, algo := range algos {
52+
if algo.Signature == signature.RSA {
53+
hasRSA = true
54+
55+
break
56+
}
57+
}
58+
assert.True(t, hasRSA, "Algorithms() should include RSA PKCS#1 v1.5 schemes")
59+
}
60+
61+
func TestAlgorithms13_IncludesRSAPSS(t *testing.T) {
62+
algos := Algorithms13()
63+
64+
// Verify DTLS 1.3 algorithms include RSA-PSS schemes
65+
hasRSAPSS := false
66+
for _, algo := range algos {
67+
if algo.Signature.IsPSS() {
68+
hasRSAPSS = true
69+
70+
break
71+
}
72+
}
73+
assert.True(t, hasRSAPSS, "Algorithms13() should include RSA-PSS schemes")
74+
75+
// Verify we still have RSA PKCS#1 v1.5 for backward compatibility
76+
hasRSA := false
77+
for _, algo := range algos {
78+
if algo.Signature == signature.RSA {
79+
hasRSA = true
80+
81+
break
82+
}
83+
}
84+
assert.True(t, hasRSA, "Algorithms13() should include RSA PKCS#1 v1.5 for backward compatibility")
85+
}
86+
87+
func TestAlgorithms13_RSAPSSBeforePKCS1(t *testing.T) {
88+
algos := Algorithms13()
89+
90+
// Find positions of first RSA-PSS and first RSA PKCS#1 schemes
91+
firstRSAPSS := -1
92+
firstRSA := -1
93+
94+
for i, algo := range algos {
95+
if firstRSAPSS == -1 && algo.Signature.IsPSS() {
96+
firstRSAPSS = i
97+
}
98+
if firstRSA == -1 && algo.Signature == signature.RSA {
99+
firstRSA = i
100+
}
101+
}
102+
103+
// In TLS 1.3, RSA-PSS should be preferred over RSA PKCS#1 v1.5
104+
assert.NotEqual(t, -1, firstRSAPSS, "Should find RSA-PSS schemes")
105+
assert.NotEqual(t, -1, firstRSA, "Should find RSA PKCS#1 schemes")
106+
assert.Less(t, firstRSAPSS, firstRSA,
107+
"RSA-PSS schemes should come before RSA PKCS#1 in Algorithms13() for TLS 1.3 preference")
108+
}

0 commit comments

Comments
 (0)