Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions ecdsa_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package signedxml

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"crypto/x509"
"encoding/asn1"
"math/big"
"testing"
)

func TestIsECDSAAlgorithm(t *testing.T) {
tests := []struct {
alg x509.SignatureAlgorithm
want bool
}{
{x509.ECDSAWithSHA1, true},
{x509.ECDSAWithSHA256, true},
{x509.ECDSAWithSHA384, true},
{x509.ECDSAWithSHA512, true},
{x509.SHA256WithRSA, false},
{x509.SHA256WithRSAPSS, false},
{x509.PureEd25519, false},
}
for _, tt := range tests {
if got := isECDSAAlgorithm(tt.alg); got != tt.want {
t.Errorf("isECDSAAlgorithm(%v) = %v, want %v", tt.alg, got, tt.want)
}
}
}

func TestConvertECDSARawToASN1(t *testing.T) {
t.Run("P256_roundtrip", func(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
msg := []byte("test data for ECDSA signature")
hash := sha256.Sum256(msg)
r, s, err := ecdsa.Sign(rand.Reader, key, hash[:])
if err != nil {
t.Fatal(err)
}

// Build raw r||s (32 bytes each for P-256)
raw := make([]byte, 64)
rBytes := r.Bytes()
sBytes := s.Bytes()
copy(raw[32-len(rBytes):32], rBytes)
copy(raw[64-len(sBytes):64], sBytes)

der, err := convertECDSARawToASN1(raw)
if err != nil {
t.Fatal(err)
}

// Parse back and verify r, s match
var parsed struct{ R, S *big.Int }
_, err = asn1.Unmarshal(der, &parsed)
if err != nil {
t.Fatalf("failed to unmarshal DER: %v", err)
}
if parsed.R.Cmp(r) != 0 {
t.Errorf("r mismatch: got %v, want %v", parsed.R, r)
}
if parsed.S.Cmp(s) != 0 {
t.Errorf("s mismatch: got %v, want %v", parsed.S, s)
}

// Verify with Go's ecdsa.Verify using the DER-decoded values
if !ecdsa.Verify(&key.PublicKey, hash[:], parsed.R, parsed.S) {
t.Error("signature verification failed after roundtrip")
}
})

t.Run("P384_roundtrip", func(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
t.Fatal(err)
}
msg := []byte("test P384")
hash := sha512.Sum384(msg)
r, s, err := ecdsa.Sign(rand.Reader, key, hash[:])
if err != nil {
t.Fatal(err)
}

raw := make([]byte, 96)
rBytes := r.Bytes()
sBytes := s.Bytes()
copy(raw[48-len(rBytes):48], rBytes)
copy(raw[96-len(sBytes):96], sBytes)

der, err := convertECDSARawToASN1(raw)
if err != nil {
t.Fatal(err)
}

var parsed struct{ R, S *big.Int }
if _, err := asn1.Unmarshal(der, &parsed); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if !ecdsa.Verify(&key.PublicKey, hash[:], parsed.R, parsed.S) {
t.Error("P-384 signature verification failed after roundtrip")
}
})

t.Run("empty_input", func(t *testing.T) {
_, err := convertECDSARawToASN1([]byte{})
if err == nil {
t.Error("expected error for empty input")
}
})

t.Run("odd_length", func(t *testing.T) {
_, err := convertECDSARawToASN1(make([]byte, 63))
if err == nil {
t.Error("expected error for odd-length input")
}
})
}
39 changes: 39 additions & 0 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package signedxml

import (
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"errors"
"fmt"
"log"
"math/big"

"github.com/beevik/etree"
)
Expand Down Expand Up @@ -166,6 +168,16 @@ func (v *Validator) validateSignature() error {
}
sig := []byte(b64)

// XML DSig encodes ECDSA signatures as raw r||s concatenation (RFC 4050),
// but Go's x509.CheckSignature expects ASN.1 DER encoding.
if isECDSAAlgorithm(v.sigAlgorithm) {
derSig, convErr := convertECDSARawToASN1(sig)
if convErr != nil {
return convErr
}
sig = derSig
}

v.signingCert = x509.Certificate{}
for _, cert := range v.Certificates {
err := cert.CheckSignature(v.sigAlgorithm, []byte(canonSignedInfo), sig)
Expand All @@ -179,6 +191,33 @@ func (v *Validator) validateSignature() error {
"SignatureValue provided")
}

// isECDSAAlgorithm returns true if the algorithm is ECDSA
func isECDSAAlgorithm(alg x509.SignatureAlgorithm) bool {
switch alg { //nolint:exhaustive
case x509.ECDSAWithSHA1, x509.ECDSAWithSHA256, x509.ECDSAWithSHA384, x509.ECDSAWithSHA512:
return true
default:
return false
}
}
Comment thread
leifj marked this conversation as resolved.

// convertECDSARawToASN1 converts an ECDSA signature from the raw r||s
// concatenation format used by XML DSig (RFC 4050) to the ASN.1 DER
// encoding expected by Go's x509.Certificate.CheckSignature.
// The input must be an even number of bytes, with r and s each occupying
// half the total length.
func convertECDSARawToASN1(raw []byte) ([]byte, error) {
if len(raw) == 0 || len(raw)%2 != 0 {
return nil, fmt.Errorf("signedxml: invalid ECDSA signature length %d", len(raw))
}
half := len(raw) / 2
r := new(big.Int).SetBytes(raw[:half])
s := new(big.Int).SetBytes(raw[half:])
return asn1.Marshal(struct {
R, S *big.Int
}{r, s})
}

func (v *Validator) loadCertificates() error {
// If v.Certificates is already populated, then the client has already set it
// to the desired cert. Otherwise, let's pull the public keys from the XML
Expand Down
Loading