Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend require() to allow plaintext to variable #3190

Closed
Sibul2k opened this issue Nov 3, 2024 · 5 comments
Closed

Extend require() to allow plaintext to variable #3190

Sibul2k opened this issue Nov 3, 2024 · 5 comments

Comments

@Sibul2k
Copy link

Sibul2k commented Nov 3, 2024

Is your feature request related to a problem? Please describe.
As discussed on the mailing list between Tom and me. Topic: "Generation of correct TLSA record variables - How?"
My issue basically could be solved by an monstrocity of escaped ", " and ' combinations. It's not readable code. It would be way more easy, if the project would allow an easy option to load plaintext from files into a variable.

Describe the solution you'd like

------ ./somefile.txt ------
CNAME("foo", "foo.example.com."),

------ ./dnscontrol.js -------

[...]
var FOO = require(./somefile.txt)

D("example.com", REG_MY_PROVIDER,
   [..],
   FOO,
END);
[...]

Result:
CNAME foo.example.com was added to D()

Describe alternatives you've considered
I had to generate this:

var  EXAMPLE_MAIL_DOMAIN_TLSA = require("/opt/dnscontrol/TLSA-25-mail.example.com.json");
var  EXAMPLE_MAIL_DOMAIN_TLSA3 = TLSA("\"_" +  EXAMPLE_MAIL_DOMAIN_TLSA["port"] + "._tcp." + EXAMPLE_MAIL_DOMAIN_TLSA["domain"] + ".\"", 3, 1, 1, "\"" +  EXAMPLE_MAIL_DOMAIN_TLSA["cert_hash"] + "\"");
var  EXAMPLE_MAIL_DOMAIN_TLSA2 = TLSA("\"_" +  EXAMPLE_MAIL_DOMAIN_TLSA["port"] + "._tcp." +  EXAMPLE_MAIL_DOMAIN_TLSA["domain"] + ".\"", 2, 1, 1, "\"" +  EXAMPLE_MAIL_DOMAIN_TLSA["root_ca_hash"] + "\"");

where all the variable parts I generated with some other script had to be reconstructed from json. I could have altered the generation of the script to just the plaintext TLSA() string.

Additional context
Should be sufficient, I hope.

Just for reference: My TLSA() script to generate the variables, if someone cares to do DANE as well. Could be altered to read the certificate file instead or talk to a webserver, not mail by changing the openssl options.

-------- ./get_smtp_tls_hash.sh -----------------

#!/bin/bash

# Defaults
PORT=25
DOMAIN=""

# get CLI-Options
while getopts "d:p:" opt; do
  case $opt in
    d) DOMAIN="$OPTARG" ;;
    p) PORT="$OPTARG" ;;
    *) echo "Usage: $0 -d <domain> [-p <port>]" && exit 1 ;;
  esac
done

# Check if DOMAIN exists
if [[ -z "$DOMAIN" ]]; then
  echo "Error: Domain (-d) is required."
  exit 1
fi

# get live certificat information
CERTIFICATE=$(echo | openssl s_client -starttls smtp -connect ${DOMAIN}:${PORT} 2>/dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p')
ROOT_CA=$(echo "$CERTIFICATE" | openssl x509 -noout -issuer -next_serial)

# SHA256-Hash calculation
CERT_HASH=$(echo "$CERTIFICATE" | openssl x509 -noout -pubkey | openssl sha256 | awk '{print $2}')
ROOT_CA_HASH=$(echo "$ROOT_CA" | openssl sha256 | awk '{print $2}')

# Generate TLSA() records
#  echo "TLSA(\"_${PORT}._tcp.${DOMAIN}.\", 3, 1, 1, \"${CERT_HASH}\")" > ./TLSA-${PORT}-3-${DOMAIN}.txt
#  echo "TLSA(\"_${PORT}._tcp.${DOMAIN}.\", 2, 1, 1, \"${ROOT_CA_HASH}\")" > ./TLSA-${PORT}-2-${DOMAIN}.txt

# Generate as JSON
echo '{
  "port": "'${PORT}'",
  "domain": "'${DOMAIN}'",
  "cert_hash": "'${CERT_HASH}'",
  "root_ca_hash": "'${ROOT_CA_HASH}'"
}' > ./TLSA-${PORT}-${DOMAIN}.json


Thank you for considering this request.

@tlimoncelli
Copy link
Contributor

How about this instead?

===== s.sh

PORT=123456
DOMAIN=example.com
CERT_HASH="MyHashMyHash"

printf '
var FOR_EXAMPLE = [
      TLSA("_%s._tcp.%s.", 3, 1, 1, "%s"),
];
' "$PORT" "$DOMAIN" "$CERT_HASH"   >tls.js


===== tls.json


var FOR_EXAMPLE = [
      TLSA("_123456._tcp.example.com.", 3, 1, 1, "hash"),
];

===== dnsconfig.js

var REG_MY_PROVIDER = NewRegistrar("none");
var DSP_MY_DNSSERVER = NewDnsProvider("none");

require("./tls.js");

D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_DNSSERVER),
  FOR_EXAMPLE,
)

@tlimoncelli
Copy link
Contributor

How about this?

Your script can generate a JSON file (perhaps with the help of a command like jq) then you can do something like:

===== tls.json

[
  {
    "cert_hash": "cert1",
    "domain": "dom1.com",
    "port": "111",
    "root_ca_hash": "hash1"
  },
  {
    "cert_hash": "cert2",
    "domain": "dom2.com",
    "port": "222",
    "root_ca_hash": "hash2"
  }
]

===== dnsconfig.js

var REG_MY_PROVIDER = NewRegistrar("none");
var DSP_MY_DNSSERVER = NewDnsProvider("none");

var FOR_EXAMPLE = require("./tls.json");

D("dom1.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_DNSSERVER),
)
D("dom2.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_DNSSERVER),
)

for (var i = 0; i < FOR_EXAMPLE.length; i++) {
  PORT=FOR_EXAMPLE[i].port;
  DOMAIN=FOR_EXAMPLE[i].domain;
  CERT_HASH=FOR_EXAMPLE[i].root_ca_hash;

  D_EXTEND(DOMAIN,
    TLSA("_" + PORT + "._tcp." + DOMAIN + ".", 3, 1, 1, CERT_HASH)
  );
}

@Sibul2k
Copy link
Author

Sibul2k commented Jan 5, 2025

Thanks for the reply and a happy new year!

I kinda like your second approach. I'd change it so something like this below, hope I made no mistake in the syntax, I didn't check this yet!

TLSA_IMPORT = require("./tls.json");

for (var i = 0; i < TLSA_IMPORT.length; i++) {
  PORT = TLSA_IMPORT[i].port;
  DOMAIN = TLSA_IMPORT[i].domain;
  CERT_HASH = TLSA_IMPORT[i].cert_hash;
  ROOT_HASH = TLSA_IMPORT[i].root_ca_hash;

  if (ROOT_HASH && CERT_HASH) {
    D_EXTEND(DOMAIN,
      TLSA("_" + PORT + "._tcp." + DOMAIN + ".", 2, 1, 1, ROOT_HASH),
      TLSA("_" + PORT + "._tcp." + DOMAIN + ".", 3, 1, 1, CERT_HASH)
    );
  } else if (CERT_HASH) {
    D_EXTEND(DOMAIN,
      TLSA("_" + PORT + "._tcp." + DOMAIN + ".", 3, 1, 1, CERT_HASH)
    );
  } else if (ROOT_HASH) {
    D_EXTEND(DOMAIN,
      TLSA("_" + PORT + "._tcp." + DOMAIN + ".", 2, 1, 1, ROOT_HASH)
    );
  }
}

But generally speaking, beeing able to just "include" the plain command would still be more generalistic in all cases.

  • Michael

@Sibul2k
Copy link
Author

Sibul2k commented Jan 20, 2025

If someone is looking for a way to implement DANE in an more automated way without (currently, as my current setup doesn't allow for this) rollovers:

Requirements:

  • DNSControl obviosly
  • jq package
  • openssl package

I updated my solution in this "issue" and came up with this monstrosity:

I call this "get_tls_hash.sh"

#!/bin/bash

# Default settings
PORT=443
DOMAIN=""

# Process CLI options
while getopts "d:p:" opt; do
  case $opt in
    d) DOMAIN="$OPTARG" ;;
    p) PORT="$OPTARG" ;;
    *) echo "Usage: $0 -d <domain> [-p <port>]" && exit 1 ;;
  esac
done

# Check if DOMAIN is set
if [[ -z "$DOMAIN" ]]; then
  echo "Error: Domain (-d) is required."
  exit 1
fi

# Create directory for output if it does not exist
mkdir -p ./vars

# Retrieve the certificate chain: Different commands based on the port
CERT_CHAIN_FILE="./vars/cert_chain_${DOMAIN}_${PORT}.pem"
if [[ "$PORT" -eq 25 ]]; then
  openssl s_client -showcerts -starttls smtp -connect "${DOMAIN}:${PORT}" < /dev/null 2>/dev/null | \
  awk '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/{print}' > "$CERT_CHAIN_FILE"
else
  openssl s_client -showcerts -connect "${DOMAIN}:${PORT}" < /dev/null 2>/dev/null | \
  awk '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/{print}' > "$CERT_CHAIN_FILE"
fi

# Check if the certificate chain was successfully retrieved
if [[ ! -s "$CERT_CHAIN_FILE" ]]; then
  echo "Error: Could not retrieve the certificate chain from ${DOMAIN}:${PORT}."
  exit 1
fi

# Extract the server certificate (first certificate in the chain)
SERVER_CERT_FILE="./vars/server_cert_${DOMAIN}_${PORT}.pem"
awk '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/' "$CERT_CHAIN_FILE" | \
  head -n $(awk '/-----END CERTIFICATE-----/{print NR; exit}' "$CERT_CHAIN_FILE") > "$SERVER_CERT_FILE"

# Compute the 3 1 1 hash for the server certificate
CERT_HASH=$(openssl x509 -in "$SERVER_CERT_FILE" -pubkey -noout | \
  openssl pkey -pubin -outform DER | \
  openssl dgst -sha256 | awk '{print $2}')

if [[ -z "$CERT_HASH" ]]; then
  echo "Error: Could not compute the 3 1 1 hash."
  exit 1
fi

# Extract the root certificate (last certificate in the chain)
ROOT_CERT_FILE="./vars/root_cert_${DOMAIN}_${PORT}.pem"
awk '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/' "$CERT_CHAIN_FILE" | \
  tail -n $(awk '/-----BEGIN CERTIFICATE-----/{n=NR} END{print NR-n+1}' "$CERT_CHAIN_FILE") > "$ROOT_CERT_FILE"

# Check if the root certificate is valid
if [[ ! -s "$ROOT_CERT_FILE" ]] || ! openssl x509 -in "$ROOT_CERT_FILE" -noout &>/dev/null; then
  echo "Warning: No valid root certificate found. Skipping 2 1 1 hash."
  ROOT_HASH="N/A"
else
  # Compute the 2 1 1 hash for the root certificate
  ROOT_HASH=$(openssl x509 -in "$ROOT_CERT_FILE" -pubkey -noout | \
    openssl pkey -pubin -outform DER | \
    openssl dgst -sha256 | awk '{print $2}')
fi

# Split the domain into TLD+SLD and subdomain
IFS='.' read -r -a DOMAIN_PARTS <<< "$DOMAIN"
if [[ ${#DOMAIN_PARTS[@]} -le 2 ]]; then
  DOMAIN_TLD_SLD="$DOMAIN"
  SUBDOMAIN=""
else
  DOMAIN_TLD_SLD="${DOMAIN_PARTS[-2]}.${DOMAIN_PARTS[-1]}"
  SUBDOMAIN="${DOMAIN_PARTS[@]:0:${#DOMAIN_PARTS[@]}-2}"
  SUBDOMAIN=$(IFS='.'; echo "$SUBDOMAIN")
fi

# JSON Database file
JSON_DB="./vars/tlsa_database.json"

# Initialize JSON database if it doesn't exist
if [[ ! -f "$JSON_DB" ]]; then
  echo "[]" > "$JSON_DB"
fi

# Update or add entry to the JSON database
jq --argjson new_entry '{
  "domain": "'"$DOMAIN_TLD_SLD"'",
  "subdomain": "'"$SUBDOMAIN"'",
  "port": "'"$PORT"'",
  "cert_hash": "'"$CERT_HASH"'",
  "root_ca_hash": "'"$ROOT_HASH"'"
}' '
  map(
    if .port == $new_entry.port and .domain == $new_entry.domain and .subdomain == $new_entry.subdomain then
      $new_entry
    else
      .
    end
  ) + (if any(.[]; .port == $new_entry.port and .domain == $new_entry.domain and .subdomain == $new_entry.subdomain) then [] else [$new_entry] end)
' "$JSON_DB" > tmp.json && mv tmp.json "$JSON_DB"

# Cleanup temporary files
rm -f "$CERT_CHAIN_FILE" "$SERVER_CERT_FILE" "$ROOT_CERT_FILE"

echo "TLSA record for ${DOMAIN}:${PORT} has been updated in the database."

Which is called by "gen_tls_hash.sh" :

#!/bin/bash

# Path to the file containing the entries
LIST="TLSA-List.txt"
# Path to the additional script
ADDITIONAL_SCRIPT="get_tls_hash.sh"

# Loop through each entry in the list
while IFS= read -r entry; do
    # Extract the host and the port
    host="${entry%%:*}" # Part before the colon
    port="${entry##*:}" # Part after the colon

    # Call the additional script with host and port
    bash "$ADDITIONAL_SCRIPT" -d "$host" -p "$port"
done < "$LIST"

TLSA-List.txt is just a plain file in this format:

example.com:443
mail.example.com:25
www.example.com:443

and the generated records are "included" (-> required) in dnscontrol.js at the very end (!) of the config file like this:

TLSA_WEB_IMPORT = require("./vars/tlsa_database.json");

for (var i = 0; i < TLSA_WEB_IMPORT.length; i++) {
  PORT = TLSA_WEB_IMPORT[i].port;
  DOMAIN = TLSA_WEB_IMPORT[i].domain;
  SUBDOMAIN = TLSA_WEB_IMPORT[i].subdomain;
  CERT_HASH = TLSA_WEB_IMPORT[i].cert_hash;
  ROOT_HASH = TLSA_WEB_IMPORT[i].root_ca_hash;

  if (ROOT_HASH && CERT_HASH && SUBDOMAIN) {
    D_EXTEND(DOMAIN,
      TLSA("_" + PORT + "._tcp." + SUBDOMAIN + "." + DOMAIN + ".", 2, 1, 1, ROOT_HASH),
      TLSA("_" + PORT + "._tcp." + SUBDOMAIN + "." + DOMAIN + ".", 3, 1, 1, CERT_HASH)
    );
  } else if (CERT_HASH && SUBDOMAIN) {
    D_EXTEND(DOMAIN,
      TLSA("_" + PORT + "._tcp." + SUBDOMAIN + "." + DOMAIN + ".", 3, 1, 1, CERT_HASH)
    );
  } else if (ROOT_HASH && SUBDOMAIN) {
    D_EXTEND(DOMAIN,
      TLSA("_" + PORT + "._tcp." + SUBDOMAIN + "." + DOMAIN + ".", 2, 1, 1, ROOT_HASH)
    );
  } else if (ROOT_HASH && CERT_HASH && !SUBDOMAIN) {
    D_EXTEND(DOMAIN,
      TLSA("_" + PORT + "._tcp." + DOMAIN + ".", 2, 1, 1, ROOT_HASH),
      TLSA("_" + PORT + "._tcp." + DOMAIN + ".", 3, 1, 1, CERT_HASH)
    );
  } else if (CERT_HASH && !SUBDOMAIN) {
    D_EXTEND(DOMAIN,
      TLSA("_" + PORT + "._tcp." + DOMAIN + ".", 3, 1, 1, CERT_HASH)
    );
  } else if (ROOT_HASH && !SUBDOMAIN) {
    D_EXTEND(DOMAIN,
      TLSA("_" + PORT + "._tcp." + DOMAIN + ".", 2, 1, 1, ROOT_HASH)
    );
  }
}

I hope somebody will find this interresting.

Michael

@tlimoncelli
Copy link
Contributor

Thanks! I'm closing this issue but github will keep this around forever.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants