diff --git a/api/schemas.yaml b/api/schemas.yaml index 025ddc8..0116246 100644 --- a/api/schemas.yaml +++ b/api/schemas.yaml @@ -873,6 +873,24 @@ components: type: string description: DKIM record content example: "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA..." + key_type: + type: string + description: "Key type from k= tag (e.g. rsa, ed25519); defaults to rsa if absent" + example: "rsa" + hash_algorithms: + type: array + items: + type: string + description: "Acceptable hash algorithms from h= tag; empty means all accepted (RFC 6376 default: sha256)" + example: ["sha256"] + signing_algorithm: + type: string + description: "Algorithm used in DKIM-Signature a= tag (e.g. rsa-sha256, ed25519-sha256)" + example: "rsa-sha256" + key_size: + type: integer + description: "Public key size in bits (RSA: 1024/2048/4096; Ed25519: always 256)" + example: 2048 valid: type: boolean description: Whether the DKIM record is valid diff --git a/pkg/analyzer/dns.go b/pkg/analyzer/dns.go index 3210dd1..6bc7c39 100644 --- a/pkg/analyzer/dns.go +++ b/pkg/analyzer/dns.go @@ -106,7 +106,7 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, headersResults *model.Head // Check DKIM records by parsing DKIM-Signature headers directly for _, sig := range parseDKIMSignatures(email.Header["Dkim-Signature"]) { - dkimRecord := d.checkDKIMRecord(sig.Domain, sig.Selector) + dkimRecord := d.checkDKIMRecord(sig) if dkimRecord != nil { if results.DkimRecords == nil { results.DkimRecords = new([]model.DKIMRecord) diff --git a/pkg/analyzer/dns_dkim.go b/pkg/analyzer/dns_dkim.go index 2ae03cb..115e347 100644 --- a/pkg/analyzer/dns_dkim.go +++ b/pkg/analyzer/dns_dkim.go @@ -23,6 +23,8 @@ package analyzer import ( "context" + "crypto/x509" + "encoding/base64" "fmt" "strings" @@ -30,17 +32,18 @@ import ( "git.happydns.org/happyDeliver/internal/utils" ) -// DKIMHeader holds the domain and selector extracted from a DKIM-Signature header. +// DKIMHeader holds the domain, selector and signing algorithm from a DKIM-Signature header. type DKIMHeader struct { - Domain string - Selector string + Domain string + Selector string + Algorithm string // from a= tag (e.g. rsa-sha256, ed25519-sha256) } -// parseDKIMSignatures extracts domain and selector from DKIM-Signature header values. +// parseDKIMSignatures extracts domain, selector and algorithm from DKIM-Signature header values. func parseDKIMSignatures(signatures []string) []DKIMHeader { var results []DKIMHeader for _, sig := range signatures { - var domain, selector string + var domain, selector, algorithm string for _, part := range strings.Split(sig, ";") { kv := strings.SplitN(strings.TrimSpace(part), "=", 2) if len(kv) != 2 { @@ -53,19 +56,61 @@ func parseDKIMSignatures(signatures []string) []DKIMHeader { domain = val case "s": selector = val + case "a": + algorithm = val } } if domain != "" && selector != "" { - results = append(results, DKIMHeader{Domain: domain, Selector: selector}) + results = append(results, DKIMHeader{Domain: domain, Selector: selector, Algorithm: algorithm}) } } return results } -// checkmodel.DKIMRecord looks up and validates DKIM record for a domain and selector -func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *model.DKIMRecord { - // DKIM records are at: selector._domainkey.domain - dkimDomain := fmt.Sprintf("%s._domainkey.%s", selector, domain) +// parseDKIMTags splits a DKIM DNS record into a tag→value map. +func parseDKIMTags(record string) map[string]string { + tags := make(map[string]string) + for _, part := range strings.Split(record, ";") { + kv := strings.SplitN(strings.TrimSpace(part), "=", 2) + if len(kv) != 2 { + continue + } + tags[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) + } + return tags +} + +// parseKeySize derives the public key bit length from a base64-encoded DER public key. +// For RSA keys it parses the PKIX structure; for Ed25519 it always returns 256. +func parseKeySize(keyType, p string) *int { + switch strings.ToLower(keyType) { + case "ed25519": + return utils.PtrTo(256) + case "rsa", "": + der, err := base64.StdEncoding.DecodeString(p) + if err != nil { + // Try without padding + der, err = base64.RawStdEncoding.DecodeString(p) + if err != nil { + return nil + } + } + pub, err := x509.ParsePKIXPublicKey(der) + if err != nil { + return nil + } + if rsaPub, ok := pub.(interface{ Size() int }); ok { + bits := rsaPub.Size() * 8 + return &bits + } + return nil + } + return nil +} + +// checkDKIMRecord looks up and validates DKIM record for a domain and selector. +func (d *DNSAnalyzer) checkDKIMRecord(h DKIMHeader) *model.DKIMRecord { + dkimDomain := fmt.Sprintf("%s._domainkey.%s", h.Selector, h.Domain) ctx, cancel := context.WithTimeout(context.Background(), d.Timeout) defer cancel() @@ -73,53 +118,83 @@ func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *model.DKIMRecord txtRecords, err := d.resolver.LookupTXT(ctx, dkimDomain) if err != nil { return &model.DKIMRecord{ - Selector: selector, - Domain: domain, - Valid: false, - Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DKIM record: %v", err)), + Selector: h.Selector, + Domain: h.Domain, + SigningAlgorithm: signingAlgorithmPtr(h.Algorithm), + Valid: false, + Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DKIM record: %v", err)), } } if len(txtRecords) == 0 { return &model.DKIMRecord{ - Selector: selector, - Domain: domain, - Valid: false, - Error: utils.PtrTo("No DKIM record found"), + Selector: h.Selector, + Domain: h.Domain, + SigningAlgorithm: signingAlgorithmPtr(h.Algorithm), + Valid: false, + Error: utils.PtrTo("No DKIM record found"), } } // Concatenate all TXT record parts (DKIM can be split) dkimRecord := strings.Join(txtRecords, "") - // Basic validation - should contain "v=DKIM1" and "p=" (public key) if !d.validateDKIM(dkimRecord) { return &model.DKIMRecord{ - Selector: selector, - Domain: domain, - Record: utils.PtrTo(dkimRecord), - Valid: false, - Error: utils.PtrTo("DKIM record appears malformed"), + Selector: h.Selector, + Domain: h.Domain, + Record: utils.PtrTo(dkimRecord), + SigningAlgorithm: signingAlgorithmPtr(h.Algorithm), + Valid: false, + Error: utils.PtrTo("DKIM record appears malformed"), } } + tags := parseDKIMTags(dkimRecord) + + keyType := tags["k"] + if keyType == "" { + keyType = "rsa" // RFC 6376 default + } + + var hashAlgorithms []string + if h, ok := tags["h"]; ok && h != "" { + for _, alg := range strings.Split(h, ":") { + if a := strings.TrimSpace(alg); a != "" { + hashAlgorithms = append(hashAlgorithms, a) + } + } + } + if hashAlgorithms == nil { + hashAlgorithms = []string{} + } + return &model.DKIMRecord{ - Selector: selector, - Domain: domain, - Record: &dkimRecord, - Valid: true, + Selector: h.Selector, + Domain: h.Domain, + Record: &dkimRecord, + KeyType: utils.PtrTo(keyType), + HashAlgorithms: &hashAlgorithms, + SigningAlgorithm: signingAlgorithmPtr(h.Algorithm), + KeySize: parseKeySize(keyType, tags["p"]), + Valid: true, } } -// validateDKIM performs basic DKIM record validation +func signingAlgorithmPtr(a string) *string { + if a == "" { + return nil + } + return &a +} + +// validateDKIM performs basic DKIM record validation. func (d *DNSAnalyzer) validateDKIM(record string) bool { - // Should contain p= tag (public key) if !strings.Contains(record, "p=") { return false } - // Often contains v=DKIM1 but not required - // If v= is present, it should be DKIM1 + // If v= is present, it must be DKIM1 if strings.Contains(record, "v=") && !strings.Contains(record, "v=DKIM1") { return false } @@ -128,21 +203,57 @@ func (d *DNSAnalyzer) validateDKIM(record string) bool { } func (d *DNSAnalyzer) calculateDKIMScore(results *model.DNSResults) (score int) { - // DKIM provides strong email authentication - if results.DkimRecords != nil && len(*results.DkimRecords) > 0 { - hasValidDKIM := false - for _, dkim := range *results.DkimRecords { - if dkim.Valid { - hasValidDKIM = true - break + if results.DkimRecords == nil || len(*results.DkimRecords) == 0 { + return 0 + } + + hasValid := false + for _, dkim := range *results.DkimRecords { + if dkim.Valid { + hasValid = true + break + } + } + + if !hasValid { + return 25 + } + + score = 100 + + // Apply security penalties on the best valid record + for _, dkim := range *results.DkimRecords { + if !dkim.Valid { + continue + } + + // SHA-1 signing is deprecated (RFC 8301) + if dkim.SigningAlgorithm != nil && strings.HasSuffix(*dkim.SigningAlgorithm, "-sha1") { + if score > 60 { + score = 60 } } - if hasValidDKIM { - score += 100 - } else { - // Partial credit if DKIM record exists but has issues - score += 25 + + // Key size penalties apply only to RSA + keyType := "" + if dkim.KeyType != nil { + keyType = strings.ToLower(*dkim.KeyType) } + if keyType == "rsa" || keyType == "" { + if dkim.KeySize != nil { + switch { + case *dkim.KeySize < 1024: + if score > 25 { + score = 25 + } + case *dkim.KeySize < 2048: + if score > 75 { + score = 75 + } + } + } + } + // Ed25519 keys (256-bit curve, ~3000-bit RSA equivalent) need no penalty. } return diff --git a/pkg/analyzer/dns_dkim_test.go b/pkg/analyzer/dns_dkim_test.go index 45da53c..40e28a5 100644 --- a/pkg/analyzer/dns_dkim_test.go +++ b/pkg/analyzer/dns_dkim_test.go @@ -22,6 +22,10 @@ package analyzer import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" "testing" "time" ) @@ -47,56 +51,56 @@ func TestParseDKIMSignatures(t *testing.T) { signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:subject:date:message-id; bh=abcdef1234567890=; b=SIGNATURE_DATA_HERE==`, }, - expected: []DKIMHeader{{Domain: "gmail.com", Selector: "20210112"}}, + expected: []DKIMHeader{{Domain: "gmail.com", Selector: "20210112", Algorithm: "rsa-sha256"}}, }, { name: "Microsoft 365 style", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=contoso.com; s=selector1; h=From:Date:Subject:Message-ID; bh=UErATeHehIIPIXPeUA==; b=SIGNATURE_DATA==`, }, - expected: []DKIMHeader{{Domain: "contoso.com", Selector: "selector1"}}, + expected: []DKIMHeader{{Domain: "contoso.com", Selector: "selector1", Algorithm: "rsa-sha256"}}, }, { name: "Tab-folded multiline (Postfix-style)", signatures: []string{ "v=1; a=rsa-sha256; c=relaxed/simple; d=nemunai.re; s=thot;\r\n\tt=1760866834; bh=YNB7c8Qgm8YGn9X1FAXTcdpO7t4YSZFiMrmpCfD/3zw=;\r\n\th=From:To:Subject;\r\n\tb=T4TFaypMpsHGYCl3PGLwmzOYRF11rYjC7lF8V5VFU+ldvG8WBpFn==", }, - expected: []DKIMHeader{{Domain: "nemunai.re", Selector: "thot"}}, + expected: []DKIMHeader{{Domain: "nemunai.re", Selector: "thot", Algorithm: "rsa-sha256"}}, }, { name: "Space-folded multiline (RFC-style)", signatures: []string{ "v=1; a=rsa-sha256; c=relaxed/relaxed;\r\n d=football.example.com; i=@football.example.com;\r\n q=dns/txt; s=test; t=1528637909; h=from:to:subject;\r\n bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r\n b=F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8Gwps==", }, - expected: []DKIMHeader{{Domain: "football.example.com", Selector: "test"}}, + expected: []DKIMHeader{{Domain: "football.example.com", Selector: "test", Algorithm: "rsa-sha256"}}, }, { name: "d= and s= on separate continuation lines", signatures: []string{ "v=1; a=rsa-sha256;\r\n\tc=relaxed/relaxed;\r\n\td=mycompany.com;\r\n\ts=selector1;\r\n\tbh=hash=;\r\n\tb=sig==", }, - expected: []DKIMHeader{{Domain: "mycompany.com", Selector: "selector1"}}, + expected: []DKIMHeader{{Domain: "mycompany.com", Selector: "selector1", Algorithm: "rsa-sha256"}}, }, { name: "No space after semicolons", signatures: []string{ `v=1;a=rsa-sha256;c=relaxed/relaxed;d=example.net;s=mail;h=from:to:subject;bh=abc=;b=xyz==`, }, - expected: []DKIMHeader{{Domain: "example.net", Selector: "mail"}}, + expected: []DKIMHeader{{Domain: "example.net", Selector: "mail", Algorithm: "rsa-sha256"}}, }, { name: "Multiple spaces after semicolons", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=myselector; bh=hash=; b=sig==`, }, - expected: []DKIMHeader{{Domain: "example.com", Selector: "myselector"}}, + expected: []DKIMHeader{{Domain: "example.com", Selector: "myselector", Algorithm: "rsa-sha256"}}, }, { name: "Ed25519 signature (RFC 8463)", signatures: []string{ "v=1; a=ed25519-sha256; c=relaxed/relaxed;\r\n d=football.example.com; i=@football.example.com;\r\n q=dns/txt; s=brisbane; t=1528637909; h=from:to:subject;\r\n bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r\n b=/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQ==", }, - expected: []DKIMHeader{{Domain: "football.example.com", Selector: "brisbane"}}, + expected: []DKIMHeader{{Domain: "football.example.com", Selector: "brisbane", Algorithm: "ed25519-sha256"}}, }, { name: "Multiple signatures (ESP double-signing)", @@ -105,8 +109,8 @@ func TestParseDKIMSignatures(t *testing.T) { `v=1; a=rsa-sha256; c=relaxed/relaxed; d=sendib.com; s=mail; h=from:to:subject; bh=hash1=; b=sig2==`, }, expected: []DKIMHeader{ - {Domain: "mydomain.com", Selector: "mail"}, - {Domain: "sendib.com", Selector: "mail"}, + {Domain: "mydomain.com", Selector: "mail", Algorithm: "rsa-sha256"}, + {Domain: "sendib.com", Selector: "mail", Algorithm: "rsa-sha256"}, }, }, { @@ -116,8 +120,8 @@ func TestParseDKIMSignatures(t *testing.T) { `v=1; a=rsa-sha256; c=relaxed/relaxed; d=football.example.com; s=test; h=from:to:subject; bh=hash=; b=rsaSig==`, }, expected: []DKIMHeader{ - {Domain: "football.example.com", Selector: "brisbane"}, - {Domain: "football.example.com", Selector: "test"}, + {Domain: "football.example.com", Selector: "brisbane", Algorithm: "ed25519-sha256"}, + {Domain: "football.example.com", Selector: "test", Algorithm: "rsa-sha256"}, }, }, { @@ -127,8 +131,8 @@ func TestParseDKIMSignatures(t *testing.T) { `v=1; a=rsa-sha256; c=relaxed/simple; d=customerdomain.io; s=ug7nbtf4gccmlpwj322ax3p6ow6fovbt; h=from:to:subject; bh=sesHash=; b=customSig==`, }, expected: []DKIMHeader{ - {Domain: "amazonses.com", Selector: "224i4yxa5dv7c2xz3womw6peuabd"}, - {Domain: "customerdomain.io", Selector: "ug7nbtf4gccmlpwj322ax3p6ow6fovbt"}, + {Domain: "amazonses.com", Selector: "224i4yxa5dv7c2xz3womw6peuabd", Algorithm: "rsa-sha256"}, + {Domain: "customerdomain.io", Selector: "ug7nbtf4gccmlpwj322ax3p6ow6fovbt", Algorithm: "rsa-sha256"}, }, }, { @@ -136,56 +140,56 @@ func TestParseDKIMSignatures(t *testing.T) { signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=mail.example.co.uk; s=dkim2025; h=from:to:subject; bh=hash=; b=sig==`, }, - expected: []DKIMHeader{{Domain: "mail.example.co.uk", Selector: "dkim2025"}}, + expected: []DKIMHeader{{Domain: "mail.example.co.uk", Selector: "dkim2025", Algorithm: "rsa-sha256"}}, }, { name: "Deeply nested subdomain", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=bounce.transactional.mail.example.com; s=s2048; h=from:to:subject; bh=hash=; b=sig==`, }, - expected: []DKIMHeader{{Domain: "bounce.transactional.mail.example.com", Selector: "s2048"}}, + expected: []DKIMHeader{{Domain: "bounce.transactional.mail.example.com", Selector: "s2048", Algorithm: "rsa-sha256"}}, }, { name: "Selector with hyphens (Microsoft 365 custom domain style)", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=selector1-contoso-com; h=from:to:subject; bh=hash=; b=sig==`, }, - expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1-contoso-com"}}, + expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1-contoso-com", Algorithm: "rsa-sha256"}}, }, { name: "Selector with dots", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=smtp.mail; h=from:to:subject; bh=hash=; b=sig==`, }, - expected: []DKIMHeader{{Domain: "example.com", Selector: "smtp.mail"}}, + expected: []DKIMHeader{{Domain: "example.com", Selector: "smtp.mail", Algorithm: "rsa-sha256"}}, }, { name: "Single-character selector", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=tiny.io; s=x; h=from:to:subject; bh=hash=; b=sig==`, }, - expected: []DKIMHeader{{Domain: "tiny.io", Selector: "x"}}, + expected: []DKIMHeader{{Domain: "tiny.io", Selector: "x", Algorithm: "rsa-sha256"}}, }, { name: "Postmark-style timestamp selector, s= before d=", signatures: []string{ `v=1; a=rsa-sha1; c=relaxed/relaxed; s=20130519032151pm; d=postmarkapp.com; h=From:Date:Subject; bh=vYFvy46eesUDGJ45hyBTH30JfN4=; b=iHeFQ+7rCiSQs3DPjR2eUSZSv4i==`, }, - expected: []DKIMHeader{{Domain: "postmarkapp.com", Selector: "20130519032151pm"}}, + expected: []DKIMHeader{{Domain: "postmarkapp.com", Selector: "20130519032151pm", Algorithm: "rsa-sha1"}}, }, { name: "d= and s= at the very end", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; h=from:to:subject; bh=hash=; b=sig==; d=example.net; s=trailing`, }, - expected: []DKIMHeader{{Domain: "example.net", Selector: "trailing"}}, + expected: []DKIMHeader{{Domain: "example.net", Selector: "trailing", Algorithm: "rsa-sha256"}}, }, { name: "Full tag set", signatures: []string{ `v=1; a=rsa-sha256; d=example.com; s=selector1; c=relaxed/simple; q=dns/txt; i=user@example.com; t=1255993973; x=1256598773; h=From:Sender:Reply-To:Subject:Date:Message-Id:To:Cc; bh=+7qxGePcmmrtZAIVQAtkSSGHfQ/ftNuvUTWJ3vXC9Zc=; b=dB85+qM+If1KGQmqMLNpqLgNtUaG5dhGjYjQD6/QXtXmViJx8tf9gLEjcHr+musLCAvr0Fsn1DA3ZLLlUxpf4AR==`, }, - expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1"}}, + expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1", Algorithm: "rsa-sha256"}}, }, { name: "Missing d= tag", @@ -216,8 +220,8 @@ func TestParseDKIMSignatures(t *testing.T) { `v=1; a=rsa-sha256; c=relaxed/relaxed; d=also-good.com; s=sel2; h=from:to; bh=hash=; b=sig==`, }, expected: []DKIMHeader{ - {Domain: "good.com", Selector: "sel1"}, - {Domain: "also-good.com", Selector: "sel2"}, + {Domain: "good.com", Selector: "sel1", Algorithm: "rsa-sha256"}, + {Domain: "also-good.com", Selector: "sel2", Algorithm: "rsa-sha256"}, }, }, } @@ -235,6 +239,9 @@ func TestParseDKIMSignatures(t *testing.T) { if result[i].Selector != tt.expected[i].Selector { t.Errorf("result[%d].Selector = %q, want %q", i, result[i].Selector, tt.expected[i].Selector) } + if result[i].Algorithm != tt.expected[i].Algorithm { + t.Errorf("result[%d].Algorithm = %q, want %q", i, result[i].Algorithm, tt.expected[i].Algorithm) + } } }) } @@ -284,3 +291,119 @@ func TestValidateDKIM(t *testing.T) { }) } } + +func TestParseDKIMTags(t *testing.T) { + tests := []struct { + name string + record string + wantTags map[string]string + }{ + { + name: "standard RSA record", + record: "v=DKIM1; k=rsa; p=MIIBI; h=sha256", + wantTags: map[string]string{"v": "DKIM1", "k": "rsa", "p": "MIIBI", "h": "sha256"}, + }, + { + name: "ed25519 record", + record: "v=DKIM1; k=ed25519; p=11qYAYKxCrfVS", + wantTags: map[string]string{"v": "DKIM1", "k": "ed25519", "p": "11qYAYKxCrfVS"}, + }, + { + name: "missing k= defaults", + record: "v=DKIM1; p=MIIBI", + wantTags: map[string]string{"v": "DKIM1", "p": "MIIBI"}, + }, + { + name: "empty record", + record: "", + wantTags: map[string]string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseDKIMTags(tt.record) + for key, want := range tt.wantTags { + if got[key] != want { + t.Errorf("tag %q = %q, want %q", key, got[key], want) + } + } + }) + } +} + +func TestParseKeySize(t *testing.T) { + // Generate a real RSA key for testing + rsaKey1024, _ := rsa.GenerateKey(rand.Reader, 1024) + rsaKey2048, _ := rsa.GenerateKey(rand.Reader, 2048) + + der1024, _ := x509.MarshalPKIXPublicKey(&rsaKey1024.PublicKey) + der2048, _ := x509.MarshalPKIXPublicKey(&rsaKey2048.PublicKey) + + p1024 := base64.StdEncoding.EncodeToString(der1024) + p2048 := base64.StdEncoding.EncodeToString(der2048) + + tests := []struct { + name string + keyType string + p string + want *int + }{ + { + name: "RSA 1024", + keyType: "rsa", + p: p1024, + want: intPtr(1024), + }, + { + name: "RSA 2048", + keyType: "rsa", + p: p2048, + want: intPtr(2048), + }, + { + name: "Ed25519 always 256", + keyType: "ed25519", + p: "11qYAYKxCrfVS", + want: intPtr(256), + }, + { + name: "Unknown key type", + keyType: "unknown", + p: "somedata", + want: nil, + }, + { + name: "Invalid RSA base64", + keyType: "rsa", + p: "!!!not-base64!!!", + want: nil, + }, + { + name: "Empty k= defaults to RSA", + keyType: "", + p: p2048, + want: intPtr(2048), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseKeySize(tt.keyType, tt.p) + if tt.want == nil { + if got != nil { + t.Errorf("parseKeySize(%q, ...) = %d, want nil", tt.keyType, *got) + } + return + } + if got == nil { + t.Fatalf("parseKeySize(%q, ...) = nil, want %d", tt.keyType, *tt.want) + } + if *got != *tt.want { + t.Errorf("parseKeySize(%q, ...) = %d, want %d", tt.keyType, *got, *tt.want) + } + }) + } +} + +func intPtr(v int) *int { return &v }