From f9fc6243d4bc103704fa5f66e9a605832d45eca2 Mon Sep 17 00:00:00 2001 From: karlism Date: Mon, 28 Jan 2019 23:26:20 +0100 Subject: [PATCH] Add SSHFP DNS record support. (#439) * Add SSHFP DNS record support. * Fix integration test. --- build/generate/featureMatrix.go | 2 + integrationTest/integration_test.go | 21 ++++++++++ models/domain.go | 2 +- models/record.go | 9 ++++- models/t_parse.go | 2 + models/t_sshfp.go | 52 ++++++++++++++++++++++++ models/target.go | 2 + pkg/js/helpers.js | 16 ++++++++ pkg/js/parse_tests/022-sshfp.js | 10 +++++ pkg/js/parse_tests/022-sshfp.json | 61 +++++++++++++++++++++++++++++ pkg/normalize/validate.go | 3 +- providers/bind/bindProvider.go | 3 ++ providers/capabilities.go | 3 ++ 13 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 models/t_sshfp.go create mode 100644 pkg/js/parse_tests/022-sshfp.js create mode 100644 pkg/js/parse_tests/022-sshfp.json diff --git a/build/generate/featureMatrix.go b/build/generate/featureMatrix.go index 4149132aa..a63bd6ec4 100644 --- a/build/generate/featureMatrix.go +++ b/build/generate/featureMatrix.go @@ -33,6 +33,7 @@ func generateFeatureMatrix() error { {"CAA", "Provider can manage CAA records"}, {"PTR", "Provider supports adding PTR records for reverse lookup zones"}, {"SRV", "Driver has explicitly implemented SRV record management"}, + {"SSHFP", "Provider can manage SSHFP records"}, {"TLSA", "Provider can manage TLSA records"}, {"TXTMulti", "Provider can manage TXT records with multiple strings"}, {"R53_ALIAS", "Provider supports Route 53 limited ALIAS"}, @@ -74,6 +75,7 @@ func generateFeatureMatrix() error { setCap("CAA", providers.CanUseCAA) setCap("PTR", providers.CanUsePTR) setCap("SRV", providers.CanUseSRV) + setCap("SSHFP", providers.CanUseSSHFP) setCap("TLSA", providers.CanUseTLSA) setCap("TXTMulti", providers.CanUseTXTMulti) setCap("R53_ALIAS", providers.CanUseRoute53Alias) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index bc888fcd5..5cbce9eea 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -262,6 +262,13 @@ func srv(name string, priority, weight, port uint16, target string) *rec { return r } +func sshfp(name string, algorithm uint8, fingerprint uint8, target string) *rec { + r := makeRec(name, target, "SSHFP") + r.SshfpAlgorithm = algorithm + r.SshfpFingerprint = fingerprint + return r +} + func txt(name, target string) *rec { // FYI: This must match the algorithm in pkg/js/helpers.js TXT. r := makeRec(name, target, "TXT") @@ -426,6 +433,20 @@ func makeTests(t *testing.T) []*TestCase { ) } + // SSHFP + if !providers.ProviderHasCabability(*providerToRun, providers.CanUseSSHFP) { + t.Log("Skipping SSHFP Tests because provider does not support them") + } else { + tests = append(tests, tc("Empty"), + tc("SSHFP record", sshfp("@", 1, 1, "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c")), + tc("SSHFP change algorithm", sshfp("@", 2, 1, "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c")), + tc("SSHFP change value", sshfp("@", 2, 1, "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc")), + tc("SSHFP change fingerprint", sshfp("@", 2, 2, "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc")), + tc("SSHFP many records", sshfp("@", 1, 1, "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"), sshfp("@", 1, 2, "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc"), sshfp("@", 2, 1, "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c")), + tc("SSHFP delete", sshfp("@", 1, 1, "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c")), + ) + } + // CAA if !providers.ProviderHasCabability(*providerToRun, providers.CanUseCAA) { t.Log("Skipping CAA Tests because provider does not support them") diff --git a/models/domain.go b/models/domain.go index 7093eb70f..0af8a493c 100644 --- a/models/domain.go +++ b/models/domain.go @@ -87,7 +87,7 @@ func (dc *DomainConfig) Punycode() error { if err != nil { return err } - case "A", "AAAA", "CAA", "TXT", "TLSA": + case "A", "AAAA", "CAA", "SSHFP", "TXT", "TLSA": // Nothing to do. default: msg := fmt.Sprintf("Punycode rtype %v unimplemented", rec.Type) diff --git a/models/record.go b/models/record.go index 346a687b6..be463ae04 100644 --- a/models/record.go +++ b/models/record.go @@ -22,6 +22,7 @@ import ( // NS // PTR // SRV +// SSHFP // TLSA // TXT // Pseudo-Types: @@ -74,6 +75,8 @@ type RecordConfig struct { SrvPort uint16 `json:"srvport,omitempty"` CaaTag string `json:"caatag,omitempty"` CaaFlag uint8 `json:"caaflag,omitempty"` + SshfpAlgorithm uint8 `json:"sshfpalgorithm,omitempty"` + SshfpFingerprint uint8 `json:"sshfpfingerprint,omitempty"` TlsaUsage uint8 `json:"tlsausage,omitempty"` TlsaSelector uint8 `json:"tlsaselector,omitempty"` TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"` @@ -218,6 +221,10 @@ func (rc *RecordConfig) ToRR() dns.RR { rr.(*dns.SRV).Weight = rc.SrvWeight rr.(*dns.SRV).Port = rc.SrvPort rr.(*dns.SRV).Target = rc.GetTargetField() + case dns.TypeSSHFP: + rr.(*dns.SSHFP).Algorithm = rc.SshfpAlgorithm + rr.(*dns.SSHFP).Type = rc.SshfpFingerprint + rr.(*dns.SSHFP).FingerPrint = rc.GetTargetField() case dns.TypeCAA: rr.(*dns.CAA).Flag = rc.CaaFlag rr.(*dns.CAA).Tag = rc.CaaTag @@ -296,7 +303,7 @@ func downcase(recs []*RecordConfig) { case "ANAME", "CNAME", "MX", "NS", "PTR", "SRV": // These record types have a target that is case insensitive, so we downcase it. r.Target = strings.ToLower(r.Target) - case "A", "AAAA", "ALIAS", "CAA", "IMPORT_TRANSFORM", "TLSA", "TXT", "SOA", "CF_REDIRECT", "CF_TEMP_REDIRECT": + case "A", "AAAA", "ALIAS", "CAA", "IMPORT_TRANSFORM", "TLSA", "TXT", "SOA", "SSHFP", "CF_REDIRECT", "CF_TEMP_REDIRECT": // These record types have a target that is case sensitive, or is an IP address. We leave them alone. // Do nothing. default: diff --git a/models/t_parse.go b/models/t_parse.go index c33edb0a1..1d89265f6 100644 --- a/models/t_parse.go +++ b/models/t_parse.go @@ -42,6 +42,8 @@ func (r *RecordConfig) PopulateFromString(rtype, contents, origin string) error return r.SetTargetMXString(contents) case "SRV": return r.SetTargetSRVString(contents) + case "SSHFP": + return r.SetTargetSSHFPString(contents) case "TLSA": return r.SetTargetTLSAString(contents) case "TXT": diff --git a/models/t_sshfp.go b/models/t_sshfp.go new file mode 100644 index 000000000..6e44b43ae --- /dev/null +++ b/models/t_sshfp.go @@ -0,0 +1,52 @@ +package models + +import ( + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// SetTargetSSHFP sets the SSHFP fields. +func (rc *RecordConfig) SetTargetSSHFP(algorithm uint8, fingerprint uint8, target string) error { + rc.SshfpAlgorithm = algorithm + rc.SshfpFingerprint = fingerprint + rc.SetTarget(target) + if rc.Type == "" { + rc.Type = "SSHFP" + } + if rc.Type != "SSHFP" { + panic("assertion failed: SetTargetSSHFP called when .Type is not SSHFP") + } + + if algorithm < 1 && algorithm > 4 { + return errors.Errorf("SSHFP algorithm (%v) is not one of 1, 2, 3 or 4", algorithm) + } + if fingerprint < 1 && fingerprint > 2 { + return errors.Errorf("SSHFP fingerprint (%v) is not one of 1 or 2", fingerprint) + } + + return nil +} + +// SetTargetSSHFPStrings is like SetTargetSSHFP but accepts strings. +func (rc *RecordConfig) SetTargetSSHFPStrings(algorithm, fingerprint, target string) error { + i64algorithm, err := strconv.ParseUint(algorithm, 10, 8) + if err != nil { + return errors.Wrap(err, "SSHFP algorithm does not fit in 8 bits") + } + i64fingerprint, err := strconv.ParseUint(fingerprint, 10, 8) + if err != nil { + return errors.Wrap(err, "SSHFP fingerprint does not fit in 8 bits") + } + return rc.SetTargetSSHFP(uint8(i64algorithm), uint8(i64fingerprint), target) +} + +// SetTargetSSHFPString is like SetTargetSSHFP but accepts one big string. +func (rc *RecordConfig) SetTargetSSHFPString(s string) error { + part := strings.Fields(s) + if len(part) != 3 { + return errors.Errorf("SSHFP value does not contain 3 fields: (%#v)", s) + } + return rc.SetTargetSSHFPStrings(part[0], part[1], StripQuotes(part[2])) +} diff --git a/models/target.go b/models/target.go index 03d44a8c2..be2afd762 100644 --- a/models/target.go +++ b/models/target.go @@ -83,6 +83,8 @@ func (rc *RecordConfig) GetTargetDebug() string { content = fmt.Sprintf("%s %s %s %d", rc.Type, rc.Name, rc.Target, rc.TTL) case "SRV": content += fmt.Sprintf(" srvpriority=%d srvweight=%d srvport=%d", rc.SrvPriority, rc.SrvWeight, rc.SrvPort) + case "SSHFP": + content += fmt.Sprintf(" sshfpalgorithm=%d sshfpfingerprint=%d", rc.SshfpAlgorithm, rc.SshfpFingerprint) case "TLSA": content += fmt.Sprintf(" tlsausage=%d tlsaselector=%d tlsamatchingtype=%d", rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType) case "CAA": diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js index c0d91a82f..1cc2ef412 100644 --- a/pkg/js/helpers.js +++ b/pkg/js/helpers.js @@ -235,6 +235,22 @@ var SRV = recordBuilder('SRV', { }, }); +// SSHFP(name,algorithm,type,value, recordModifiers...) +var SSHFP = recordBuilder('SSHFP', { + args: [ + ['name', _.isString], + ['algorithm', _.isNumber], + ['fingerprint', _.isNumber], + ['value', _.isString], + ], + transform: function(record, args, modifiers) { + record.name = args.name; + record.sshfpalgorithm = args.algorithm; + record.sshfpfingerprint = args.fingerprint; + record.target = args.value; + }, +}); + // name, usage, selector, matchingtype, certificate var TLSA = recordBuilder('TLSA', { args: [ diff --git a/pkg/js/parse_tests/022-sshfp.js b/pkg/js/parse_tests/022-sshfp.js new file mode 100644 index 000000000..876916512 --- /dev/null +++ b/pkg/js/parse_tests/022-sshfp.js @@ -0,0 +1,10 @@ +D("foo.com","none", + SSHFP("@",1,1,"66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"), + SSHFP("@",1,2,"745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc"), + SSHFP("@",2,1,"66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"), + SSHFP("@",2,2,"745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc"), + SSHFP("@",3,1,"66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"), + SSHFP("@",3,2,"745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc"), + SSHFP("@",4,1,"66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"), + SSHFP("@",4,2,"745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc") +); diff --git a/pkg/js/parse_tests/022-sshfp.json b/pkg/js/parse_tests/022-sshfp.json new file mode 100644 index 000000000..43c876540 --- /dev/null +++ b/pkg/js/parse_tests/022-sshfp.json @@ -0,0 +1,61 @@ +{ + "registrars": [], + "dns_providers": [], + "domains": [ + { + "name": "foo.com", + "registrar": "none", + "dnsProviders": {}, + "records": [ + { + "type": "SSHFP", + "algorithm": 1, + "fingerprint": 1, + "value": "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c" + }, + { + "type": "SSHFP", + "algorithm": 1, + "fingerprint": 2, + "value": "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc" + }, + { + "type": "SSHFP", + "algorithm": 2, + "fingerprint": 1, + "value": "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c" + }, + { + "type": "SSHFP", + "algorithm": 2, + "fingerprint": 2, + "value": "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc" + }, + { + "type": "SSHFP", + "algorithm": 3, + "fingerprint": 1, + "value": "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c" + }, + { + "type": "SSHFP", + "algorithm": 3, + "fingerprint": 2, + "value": "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc" + }, + { + "type": "SSHFP", + "algorithm": 4, + "fingerprint": 1, + "value": "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c" + }, + { + "type": "SSHFP", + "algorithm": 4, + "fingerprint": 2, + "value": "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc" + } + ] + } + ] +} diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go index f91fd1fc0..8b73c5030 100644 --- a/pkg/normalize/validate.go +++ b/pkg/normalize/validate.go @@ -58,6 +58,7 @@ func validateRecordTypes(rec *models.RecordConfig, domain string, pTypes []strin "IMPORT_TRANSFORM": false, "MX": true, "SRV": true, + "SSHFP": true, "TXT": true, "NS": true, "PTR": true, @@ -160,7 +161,7 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) { check(checkTarget(target)) case "SRV": check(checkTarget(target)) - case "TXT", "IMPORT_TRANSFORM", "CAA", "TLSA": + case "TXT", "IMPORT_TRANSFORM", "CAA", "SSHFP", "TLSA": default: if rec.Metadata["orig_custom_type"] != "" { // it is a valid custom type. We perform no validation on target diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go index 524c928a7..703df9eab 100644 --- a/providers/bind/bindProvider.go +++ b/providers/bind/bindProvider.go @@ -34,6 +34,7 @@ var features = providers.DocumentationNotes{ providers.CanUseCAA: providers.Can(), providers.CanUsePTR: providers.Can(), providers.CanUseSRV: providers.Can(), + providers.CanUseSSHFP: providers.Can(), providers.CanUseTLSA: providers.Can(), providers.CanUseTXTMulti: providers.Can(), providers.CantUseNOPURGE: providers.Cannot(), @@ -137,6 +138,8 @@ func rrToRecord(rr dns.RR, origin string, replaceSerial uint32) (models.RecordCo // FIXME(tlim): SOA should be handled by splitting out the fields. case *dns.SRV: panicInvalid(rc.SetTargetSRV(v.Priority, v.Weight, v.Port, v.Target)) + case *dns.SSHFP: + panicInvalid(rc.SetTargetSSHFP(v.Algorithm, v.Type, v.FingerPrint)) case *dns.TLSA: panicInvalid(rc.SetTargetTLSA(v.Usage, v.Selector, v.MatchingType, v.Certificate)) case *dns.TXT: diff --git a/providers/capabilities.go b/providers/capabilities.go index 7f6dde660..41d8563fb 100644 --- a/providers/capabilities.go +++ b/providers/capabilities.go @@ -22,6 +22,9 @@ const ( // CanUseSRV indicates the provider can handle SRV records CanUseSRV + // CanUseSSHFP indicates the provider can handle SSHFP records + CanUseSSHFP + // CanUseTLSA indicates the provider can handle TLSA records CanUseTLSA