Initial commit
This commit is contained in:
commit
19296f4188
18 changed files with 2562 additions and 0 deletions
239
checker/types.go
Normal file
239
checker/types.go
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
// Package checker implements the OPENPGPKEY/SMIMEA DANE checker for
|
||||
// happyDomain. It runs a comprehensive testsuite on the DNS-published
|
||||
// OpenPGP key (RFC 7929) or S/MIME certificate (RFC 8162) corresponding
|
||||
// to an abstract.OpenPGP or abstract.SMimeCert service, and turns the
|
||||
// results into structured findings + a remediation-oriented HTML report.
|
||||
package checker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ObservationKey is the key this checker publishes. The payload is an
|
||||
// *EmailKeyData JSON document.
|
||||
const ObservationKey = "openpgpkey_smimea"
|
||||
|
||||
// Supported service types.
|
||||
const (
|
||||
ServiceOpenPGP = "abstract.OpenPGP"
|
||||
ServiceSMimeCert = "abstract.SMimeCert"
|
||||
KindOpenPGPKey = "openpgpkey"
|
||||
KindSMIMEA = "smimea"
|
||||
OpenPGPKeyPrefix = "_openpgpkey"
|
||||
SMIMEACertPrefix = "_smimecert"
|
||||
DANEOwnerHashSize = 28 // bytes of SHA-256 kept as the owner prefix
|
||||
)
|
||||
|
||||
// Severity classifies a finding emitted by the checker.
|
||||
type Severity string
|
||||
|
||||
const (
|
||||
SeverityInfo Severity = "info"
|
||||
SeverityWarn Severity = "warn"
|
||||
SeverityCrit Severity = "crit"
|
||||
)
|
||||
|
||||
// Finding codes surfaced by the checker. These strings are stable; the
|
||||
// UI keys remediation templates off them.
|
||||
const (
|
||||
// DNS-level.
|
||||
CodeDNSQueryFailed = "dns_query_failed"
|
||||
CodeDNSNoRecord = "dns_no_record"
|
||||
CodeDNSRecordMismatch = "dns_record_mismatch"
|
||||
CodeDNSNotSecure = "dnssec_not_validated"
|
||||
CodeOwnerHashMismatch = "owner_hash_mismatch"
|
||||
|
||||
// OpenPGP.
|
||||
CodePGPParseError = "pgp_parse_error"
|
||||
CodePGPNoEntity = "pgp_no_entity"
|
||||
CodePGPRevoked = "pgp_primary_revoked"
|
||||
CodePGPExpired = "pgp_primary_expired"
|
||||
CodePGPExpiringSoon = "pgp_primary_expiring_soon"
|
||||
CodePGPWeakAlgorithm = "pgp_weak_algorithm"
|
||||
CodePGPWeakKeySize = "pgp_weak_key_size"
|
||||
CodePGPNoEncryption = "pgp_no_encryption_subkey"
|
||||
CodePGPNoIdentity = "pgp_no_identity"
|
||||
CodePGPUIDMismatch = "pgp_uid_mismatch"
|
||||
CodePGPMultipleEntities = "pgp_multiple_entities"
|
||||
CodePGPRecordTooLarge = "pgp_record_too_large"
|
||||
|
||||
// SMIMEA.
|
||||
CodeSMIMEABadUsage = "smimea_bad_usage"
|
||||
CodeSMIMEABadSelector = "smimea_bad_selector"
|
||||
CodeSMIMEABadMatchType = "smimea_bad_match_type"
|
||||
CodeSMIMEACertParseError = "smimea_cert_parse_error"
|
||||
CodeSMIMEACertExpired = "smimea_cert_expired"
|
||||
CodeSMIMEACertExpiringSoon = "smimea_cert_expiring_soon"
|
||||
CodeSMIMEACertNotYetValid = "smimea_cert_not_yet_valid"
|
||||
CodeSMIMEANoEmailProtection = "smimea_no_email_protection_eku"
|
||||
CodeSMIMEAEmailMismatch = "smimea_email_mismatch"
|
||||
CodeSMIMEAWeakKeySize = "smimea_weak_key_size"
|
||||
CodeSMIMEAWeakSignatureAlg = "smimea_weak_signature_algorithm"
|
||||
CodeSMIMEANoKeyUsage = "smimea_missing_key_usage"
|
||||
CodeSMIMEAChainUntrusted = "smimea_chain_untrusted"
|
||||
CodeSMIMEASelfSigned = "smimea_self_signed"
|
||||
CodeSMIMEAHashOnly = "smimea_hash_only"
|
||||
)
|
||||
|
||||
// Finding describes a single observation produced while running the
|
||||
// testsuite.
|
||||
type Finding struct {
|
||||
Code string `json:"code"`
|
||||
Severity Severity `json:"severity"`
|
||||
Message string `json:"message"`
|
||||
// Fix carries a short, user-facing hint describing how to address the
|
||||
// issue. The HTML report falls back on generic Fix text keyed by Code
|
||||
// when this field is empty.
|
||||
Fix string `json:"fix,omitempty"`
|
||||
}
|
||||
|
||||
// EmailKeyData is the observation payload written under ObservationKey.
|
||||
type EmailKeyData struct {
|
||||
// Kind is "openpgpkey" or "smimea".
|
||||
Kind string `json:"kind"`
|
||||
|
||||
// Domain is the FQDN of the zone (origin) that publishes the record.
|
||||
Domain string `json:"domain"`
|
||||
|
||||
// Subdomain is the relative name below Domain where the service sits
|
||||
// (empty for the zone apex).
|
||||
Subdomain string `json:"subdomain,omitempty"`
|
||||
|
||||
// Username is the local part copied from the service. When empty,
|
||||
// the username-hash-prefix verification is skipped.
|
||||
Username string `json:"username,omitempty"`
|
||||
|
||||
// ExpectedOwner is the FQDN at which the record should be
|
||||
// published, per RFC 7929 / RFC 8162.
|
||||
ExpectedOwner string `json:"expected_owner,omitempty"`
|
||||
|
||||
// QueriedOwner is the FQDN actually queried (may differ from
|
||||
// ExpectedOwner if the service record already carries its own name).
|
||||
QueriedOwner string `json:"queried_owner,omitempty"`
|
||||
|
||||
// Resolver is the DNS server that answered the lookup.
|
||||
Resolver string `json:"resolver,omitempty"`
|
||||
|
||||
// DNSSECSecure is true when the validating resolver set the AD flag
|
||||
// on the answer. Nil means the lookup did not complete.
|
||||
DNSSECSecure *bool `json:"dnssec_secure,omitempty"`
|
||||
|
||||
// RecordCount is the number of records returned at QueriedOwner.
|
||||
RecordCount int `json:"record_count"`
|
||||
|
||||
// OpenPGP is populated for kind=openpgpkey.
|
||||
OpenPGP *OpenPGPInfo `json:"openpgp,omitempty"`
|
||||
|
||||
// SMIMEA is populated for kind=smimea.
|
||||
SMIMEA *SMIMEAInfo `json:"smimea,omitempty"`
|
||||
|
||||
Findings []Finding `json:"findings"`
|
||||
|
||||
CollectedAt time.Time `json:"collected_at"`
|
||||
}
|
||||
|
||||
// OpenPGPInfo summarises the OpenPGP key observed in the record.
|
||||
type OpenPGPInfo struct {
|
||||
// RawSize is the length in bytes of the transport key material.
|
||||
RawSize int `json:"raw_size"`
|
||||
|
||||
// PrimaryAlgorithm is the name of the primary key's algorithm,
|
||||
// e.g. "RSA", "Ed25519", "ECDSA-NIST-P-256".
|
||||
PrimaryAlgorithm string `json:"primary_algorithm,omitempty"`
|
||||
|
||||
// PrimaryBits is the key size in bits for the primary key (0 when
|
||||
// the algorithm is of fixed size, e.g. Ed25519).
|
||||
PrimaryBits int `json:"primary_bits,omitempty"`
|
||||
|
||||
// Fingerprint is the hex-encoded OpenPGP fingerprint.
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
|
||||
// KeyID is the short 64-bit key id, hex.
|
||||
KeyID string `json:"key_id,omitempty"`
|
||||
|
||||
// UIDs lists the User ID strings carried in the key.
|
||||
UIDs []string `json:"uids,omitempty"`
|
||||
|
||||
// CreatedAt is the primary key creation time.
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
|
||||
// ExpiresAt is the primary key expiration time (zero for "never").
|
||||
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||
|
||||
// Revoked is true when the primary key carries a revocation signature.
|
||||
Revoked bool `json:"revoked,omitempty"`
|
||||
|
||||
// Subkeys describes the subordinate keys.
|
||||
Subkeys []SubkeyInfo `json:"subkeys,omitempty"`
|
||||
|
||||
// EntityCount is the number of OpenPGP entities parsed from the
|
||||
// record. RFC 7929 recommends a single entity per record.
|
||||
EntityCount int `json:"entity_count"`
|
||||
|
||||
// HasEncryptionCapability is true when at least one non-revoked,
|
||||
// non-expired key in the entity advertises encryption usage flags.
|
||||
HasEncryptionCapability bool `json:"has_encryption_capability"`
|
||||
}
|
||||
|
||||
// SubkeyInfo summarises one OpenPGP subkey.
|
||||
type SubkeyInfo struct {
|
||||
Algorithm string `json:"algorithm"`
|
||||
Bits int `json:"bits,omitempty"`
|
||||
CanSign bool `json:"can_sign,omitempty"`
|
||||
CanEncrypt bool `json:"can_encrypt,omitempty"`
|
||||
CanAuth bool `json:"can_auth,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||
Revoked bool `json:"revoked,omitempty"`
|
||||
}
|
||||
|
||||
// SMIMEAInfo summarises the S/MIME record.
|
||||
type SMIMEAInfo struct {
|
||||
Usage uint8 `json:"usage"`
|
||||
Selector uint8 `json:"selector"`
|
||||
MatchingType uint8 `json:"matching_type"`
|
||||
|
||||
// Certificate is populated when the record carries a full X.509
|
||||
// certificate (selector 0, matching type 0). For selector 1 + type 0
|
||||
// only PublicKey is populated. For matching types 1/2, neither is
|
||||
// populated; only the digest is transported.
|
||||
Certificate *CertInfo `json:"certificate,omitempty"`
|
||||
PublicKey *PubKeyInfo `json:"public_key,omitempty"`
|
||||
|
||||
// HashHex, when set, is the hex digest embedded in the record.
|
||||
HashHex string `json:"hash_hex,omitempty"`
|
||||
}
|
||||
|
||||
// CertInfo summarises an X.509 certificate.
|
||||
type CertInfo struct {
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Issuer string `json:"issuer,omitempty"`
|
||||
SerialHex string `json:"serial_hex,omitempty"`
|
||||
NotBefore time.Time `json:"not_before,omitempty"`
|
||||
NotAfter time.Time `json:"not_after,omitempty"`
|
||||
SignatureAlgorithm string `json:"signature_algorithm,omitempty"`
|
||||
PublicKeyAlgorithm string `json:"public_key_algorithm,omitempty"`
|
||||
PublicKeyBits int `json:"public_key_bits,omitempty"`
|
||||
EmailAddresses []string `json:"email_addresses,omitempty"`
|
||||
DNSNames []string `json:"dns_names,omitempty"`
|
||||
HasEmailProtectionEKU bool `json:"has_email_protection_eku,omitempty"`
|
||||
HasDigitalSignature bool `json:"has_digital_signature,omitempty"`
|
||||
HasKeyEncipherment bool `json:"has_key_encipherment,omitempty"`
|
||||
IsSelfSigned bool `json:"is_self_signed,omitempty"`
|
||||
IsCA bool `json:"is_ca,omitempty"`
|
||||
}
|
||||
|
||||
// PubKeyInfo summarises an SPKI-only SMIMEA record.
|
||||
type PubKeyInfo struct {
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
Bits int `json:"bits,omitempty"`
|
||||
}
|
||||
|
||||
// serviceMessage is a minimal mirror of happyDomain's ServiceMessage JSON
|
||||
// envelope used to carry the auto-filled service.
|
||||
type serviceMessage struct {
|
||||
Type string `json:"_svctype"`
|
||||
Domain string `json:"_domain"`
|
||||
Service json.RawMessage `json:"Service"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue