// Package certs manages TLS certificates (docs/certificates.md): ones Jarl // obtains and renews itself over ACME, answering the DNS-01 challenges in // its own zones or at Porkbun, and ones you renew yourself that Jarl only // reads. A certificate is used for jarld's HTTPS ("web") and/or written // where named can read it ("dns", for DNS over TLS and HTTPS). package certs import ( "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" "crypto/tls" "crypto/x509" "errors" "fmt" "strings" "time" ) // Info is what a certificate says about itself. type Info struct { Names []string NotBefore time.Time NotAfter time.Time Issuer string Serial string KeyType string // ec256, ec384, rsa2048, ... Leaf *x509.Certificate // Warnings are problems that don't stop it working everywhere (a // missing intermediate certificate). Warnings []string } // Inspect checks that certPEM (the certificate, then its intermediates) // and keyPEM belong together and returns what the certificate says, and // the pair ready to serve. func Inspect(certPEM, keyPEM []byte) (*Info, *tls.Certificate, error) { pair, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { msg := err.Error() switch { case strings.Contains(msg, "private key does not match public key"): return nil, nil, errors.New("the key doesn't belong to the certificate") case strings.Contains(msg, "failed to find any PEM data in certificate"): return nil, nil, errors.New("no certificate found (expected PEM: -----BEGIN CERTIFICATE-----)") case strings.Contains(msg, "failed to find any PEM data in key"), strings.Contains(msg, "failed to find PEM block with type ending in \"PRIVATE KEY\""): return nil, nil, errors.New("no private key found (expected PEM: -----BEGIN ... PRIVATE KEY-----)") } return nil, nil, err } leaf := pair.Leaf if leaf == nil { if leaf, err = x509.ParseCertificate(pair.Certificate[0]); err != nil { return nil, nil, err } pair.Leaf = leaf } in := &Info{NotBefore: leaf.NotBefore, NotAfter: leaf.NotAfter, Serial: fmt.Sprintf("%x", leaf.SerialNumber), Leaf: leaf, KeyType: keyType(leaf)} in.Names = append(in.Names, leaf.DNSNames...) for _, ip := range leaf.IPAddresses { in.Names = append(in.Names, ip.String()) } if len(in.Names) == 0 && leaf.Subject.CommonName != "" { in.Names = []string{leaf.Subject.CommonName} } in.Issuer = leaf.Issuer.CommonName if len(leaf.Issuer.Organization) > 0 { if in.Issuer == "" { in.Issuer = leaf.Issuer.Organization[0] } else { in.Issuer = leaf.Issuer.Organization[0] + " " + in.Issuer } } selfSigned := leaf.Issuer.String() == leaf.Subject.String() if len(pair.Certificate) == 1 && !selfSigned { in.Warnings = append(in.Warnings, "the file holds only the certificate, without its intermediate certificate: many clients will reject it; use the full chain (fullchain.pem)") } return in, &pair, nil } func keyType(c *x509.Certificate) string { switch k := c.PublicKey.(type) { case *ecdsa.PublicKey: return fmt.Sprintf("ec%d", k.Curve.Params().BitSize) case *rsa.PublicKey: return fmt.Sprintf("rsa%d", k.N.BitLen()) case ed25519.PublicKey: return "ed25519" } return "unknown" } // Covers reports whether the certificate names cover every name in want // (a wildcard covers one label). func Covers(names, want []string) bool { for _, w := range want { if !covered(names, w) { return false } } return true } func covered(names []string, want string) bool { want = strings.ToLower(strings.TrimSuffix(want, ".")) for _, n := range names { n = strings.ToLower(n) if n == want { return true } if base, ok := strings.CutPrefix(n, "*."); ok && !strings.HasPrefix(want, "*.") { if _, rest, ok := strings.Cut(want, "."); ok && rest == base { return true } } } return false }