Skip to content
Open
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
105 changes: 105 additions & 0 deletions sign/keysize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package sign

import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"errors"
"fmt"
)

var (
ErrNilSigner = errors.New("signer cannot be nil")
ErrNilPublicKey = errors.New("public key cannot be nil")
ErrNilCertificate = errors.New("certificate cannot be nil")
ErrUnsupportedKey = errors.New("unsupported key type")
ErrKeyMismatch = errors.New("signer public key does not match certificate")
)

// SignatureSize returns the maximum signature size in bytes for the given signer.
// Do not use Certificate.SignatureAlgorithm for this - that's how the CA signed
// the cert, not the size of signatures this key produces.
func SignatureSize(signer crypto.Signer) (int, error) {
if signer == nil {
return 0, ErrNilSigner
}

pub := signer.Public()
if pub == nil {
return 0, ErrNilPublicKey
}

return PublicKeySignatureSize(pub)
}

// PublicKeySignatureSize returns the maximum signature size for a public key.
func PublicKeySignatureSize(pub crypto.PublicKey) (int, error) {
if pub == nil {
return 0, ErrNilPublicKey
}

switch k := pub.(type) {
case *rsa.PublicKey:
if k.N == nil {
return 0, fmt.Errorf("%w: RSA key has nil modulus", ErrUnsupportedKey)
}
return k.Size(), nil

case *ecdsa.PublicKey:
if k.Curve == nil {
return 0, fmt.Errorf("%w: ECDSA key has nil curve", ErrUnsupportedKey)
}
// ECDSA signatures are DER-encoded as SEQUENCE { r INTEGER, s INTEGER } per RFC 3279 Section 2.2.3.
// Max size: 2 coords + 9 bytes overhead (SEQUENCE tag/len, two INTEGER tag/len, two padding bytes)
coordSize := (k.Curve.Params().BitSize + 7) / 8
return 2*coordSize + 9, nil

case ed25519.PublicKey:
return ed25519.SignatureSize, nil

default:
return 0, fmt.Errorf("%w: %T", ErrUnsupportedKey, pub)
}
}

// DefaultSignatureSize is the fallback for unrecognized key types.
const DefaultSignatureSize = 8192

// ValidateSignerCertificateMatch checks that the signer's public key matches the certificate.
func ValidateSignerCertificateMatch(signer crypto.Signer, cert *x509.Certificate) error {
if signer == nil {
return ErrNilSigner
}
if cert == nil {
return ErrNilCertificate
}

signerPub := signer.Public()
if signerPub == nil {
return ErrNilPublicKey
}

signerPubBytes, err := x509.MarshalPKIXPublicKey(signerPub)
if err != nil {
return fmt.Errorf("failed to marshal signer public key: %w", err)
}

certPubBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err != nil {
return fmt.Errorf("failed to marshal certificate public key: %w", err)
}

if len(signerPubBytes) != len(certPubBytes) {
return ErrKeyMismatch
}

for i := range signerPubBytes {
if signerPubBytes[i] != certPubBytes[i] {
return ErrKeyMismatch
}
}

return nil
}
266 changes: 266 additions & 0 deletions sign/keysize_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
package sign

import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"errors"
"math/big"
"testing"
"time"
)

func TestSignatureSize(t *testing.T) {
tests := []struct {
name string
keyBits int
keyType string
wantSize int
}{
{"RSA-1024", 1024, "RSA", 128},
{"RSA-2048", 2048, "RSA", 256},
{"RSA-3072", 3072, "RSA", 384},
{"RSA-4096", 4096, "RSA", 512},
{"ECDSA-P256", 256, "ECDSA", 73}, // 2*32 + 9 = 73 (DER overhead)
{"ECDSA-P384", 384, "ECDSA", 105}, // 2*48 + 9 = 105
{"ECDSA-P521", 521, "ECDSA", 141}, // 2*66 + 9 = 141
{"Ed25519", 0, "Ed25519", 64},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var signer crypto.Signer
var err error

switch tt.keyType {
case "RSA":
signer, err = rsa.GenerateKey(rand.Reader, tt.keyBits)
case "ECDSA":
var curve elliptic.Curve
switch tt.keyBits {
case 256:
curve = elliptic.P256()
case 384:
curve = elliptic.P384()
case 521:
curve = elliptic.P521()
}
signer, err = ecdsa.GenerateKey(curve, rand.Reader)
case "Ed25519":
_, signer, err = ed25519.GenerateKey(rand.Reader)
}

if err != nil {
t.Fatalf("key generation failed: %v", err)
}

gotSize, err := SignatureSize(signer)
if err != nil {
t.Fatalf("SignatureSize failed: %v", err)
}

if gotSize != tt.wantSize {
t.Errorf("SignatureSize() = %d, want %d", gotSize, tt.wantSize)
}
})
}
}

func TestSignatureSize_Errors(t *testing.T) {
t.Run("nil signer", func(t *testing.T) {
_, err := SignatureSize(nil)
if !errors.Is(err, ErrNilSigner) {
t.Errorf("expected ErrNilSigner, got %v", err)
}
})

t.Run("unsupported key type", func(t *testing.T) {
_, err := PublicKeySignatureSize(struct{}{})
if !errors.Is(err, ErrUnsupportedKey) {
t.Errorf("expected ErrUnsupportedKey, got %v", err)
}
})

t.Run("nil public key", func(t *testing.T) {
_, err := PublicKeySignatureSize(nil)
if !errors.Is(err, ErrNilPublicKey) {
t.Errorf("expected ErrNilPublicKey, got %v", err)
}
})
}

func TestPublicKeySignatureSize_RSA(t *testing.T) {
// Test that RSA public key size calculation matches key.Size()
for _, bits := range []int{1024, 2048, 3072, 4096} {
t.Run("RSA-"+string(rune('0'+bits/1000))+string(rune('0'+(bits%1000)/100))+string(rune('0'+(bits%100)/10))+string(rune('0'+bits%10)), func(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
t.Fatalf("key generation failed: %v", err)
}

size, err := PublicKeySignatureSize(&key.PublicKey)
if err != nil {
t.Fatalf("PublicKeySignatureSize failed: %v", err)
}

if size != key.Size() {
t.Errorf("PublicKeySignatureSize() = %d, want %d (key.Size())", size, key.Size())
}

if size != bits/8 {
t.Errorf("PublicKeySignatureSize() = %d, want %d (bits/8)", size, bits/8)
}
})
}
}

func createTestCertificate(t *testing.T, key crypto.Signer) *x509.Certificate {
t.Helper()

template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "Test Certificate",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
}

certDER, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
if err != nil {
t.Fatalf("failed to create certificate: %v", err)
}

cert, err := x509.ParseCertificate(certDER)
if err != nil {
t.Fatalf("failed to parse certificate: %v", err)
}

return cert
}

func TestValidateSignerCertificateMatch(t *testing.T) {
// Generate matching pair
key1, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("failed to generate key1: %v", err)
}
cert1 := createTestCertificate(t, key1)

// Generate mismatched pair
key2, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("failed to generate key2: %v", err)
}

t.Run("matching keys", func(t *testing.T) {
err := ValidateSignerCertificateMatch(key1, cert1)
if err != nil {
t.Errorf("expected no error for matching keys, got %v", err)
}
})

t.Run("mismatched keys", func(t *testing.T) {
err := ValidateSignerCertificateMatch(key2, cert1)
if !errors.Is(err, ErrKeyMismatch) {
t.Errorf("expected ErrKeyMismatch, got %v", err)
}
})

t.Run("nil signer", func(t *testing.T) {
err := ValidateSignerCertificateMatch(nil, cert1)
if !errors.Is(err, ErrNilSigner) {
t.Errorf("expected ErrNilSigner, got %v", err)
}
})

t.Run("nil certificate", func(t *testing.T) {
err := ValidateSignerCertificateMatch(key1, nil)
if !errors.Is(err, ErrNilCertificate) {
t.Errorf("expected ErrNilCertificate, got %v", err)
}
})
}

func TestValidateSignerCertificateMatch_DifferentKeyTypes(t *testing.T) {
// Generate ECDSA key and certificate
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("failed to generate EC key: %v", err)
}
ecCert := createTestCertificate(t, ecKey)

// Generate RSA key
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("failed to generate RSA key: %v", err)
}

t.Run("RSA signer with EC certificate", func(t *testing.T) {
err := ValidateSignerCertificateMatch(rsaKey, ecCert)
if !errors.Is(err, ErrKeyMismatch) {
t.Errorf("expected ErrKeyMismatch for mismatched key types, got %v", err)
}
})

t.Run("EC signer with EC certificate", func(t *testing.T) {
err := ValidateSignerCertificateMatch(ecKey, ecCert)
if err != nil {
t.Errorf("expected no error for matching EC keys, got %v", err)
}
})
}

// BenchmarkSignatureSize benchmarks the signature size calculation
func BenchmarkSignatureSize(b *testing.B) {
rsaKey, _ := rsa.GenerateKey(rand.Reader, 4096)
ecKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
_, edKey, _ := ed25519.GenerateKey(rand.Reader)
Comment on lines +223 to +225
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These benchmarks ignore key-generation errors. If rsa.GenerateKey / ecdsa.GenerateKey / ed25519.GenerateKey fails (e.g., entropy/read failure), the benchmark may panic later when using a nil key. Handle the errors (fail the benchmark) to keep behavior deterministic.

Copilot uses AI. Check for mistakes.

b.Run("RSA-4096", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = SignatureSize(rsaKey)
}
})

b.Run("ECDSA-P256", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = SignatureSize(ecKey)
}
})

b.Run("Ed25519", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = SignatureSize(edKey)
}
})
}

// BenchmarkValidateSignerCertificateMatch benchmarks the validation function
func BenchmarkValidateSignerCertificateMatch(b *testing.B) {
rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048)

// Create a certificate manually for benchmarking
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Test"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
}
certDER, _ := x509.CreateCertificate(rand.Reader, template, template, rsaKey.Public(), rsaKey)
cert, _ := x509.ParseCertificate(certDER)

Comment on lines +259 to +261
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These certificate creation/parsing calls ignore errors. If either fails, cert can be nil and ValidateSignerCertificateMatch may panic or skew benchmark results. Consider checking and failing the benchmark on error before b.ResetTimer().

Copilot uses AI. Check for mistakes.
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ValidateSignerCertificateMatch(rsaKey, cert)
}
}
Loading