Add DNS API plugin for Oracle Cloud Infrastructure DNS Service

This plugin is has noticeably more required fields than most
other plugins due to the requirement that all requests to
the OCI REST API must be cryptographically signed by the client
using the draft standard proposed in draft-cavage-http-signatures-08[1].

The OCI specific implementation details of the draft standard are
documented in the Developer Guide[2].

NOTE: there is maximum allowed clock skew of five minutes between the
client and the API endpoint. Requests will be denied if the skew is
greater.

This PR also includes a minor tweak to the Solaris job in the DNS
workflow so that it uses the pre-installed GNU tools, curl and OpenSSL 1.1.1.
Without these changes, the signature generation function does not
work on Solaris.

[1]: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-08
[2]: https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm#five

Signed-off-by: Avi Miller <avi.miller@oracle.com>
This commit is contained in:
Avi Miller 2021-06-04 19:20:23 +10:00
parent 43cb230f19
commit 6f88c81616
No known key found for this signature in database
GPG Key ID: 66D6066620F03B05
2 changed files with 251 additions and 5 deletions

View File

@ -59,7 +59,7 @@ jobs:
run: cd .. && git clone https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ run: cd .. && git clone https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Set env file - name: Set env file
run: | run: |
cd ../acmetest cd ../acmetest
if [ "${{ secrets.TokenName1}}" ] ; then if [ "${{ secrets.TokenName1}}" ] ; then
echo "${{ secrets.TokenName1}}=${{ secrets.TokenValue1}}" >> env.list echo "${{ secrets.TokenName1}}=${{ secrets.TokenValue1}}" >> env.list
fi fi
@ -75,7 +75,7 @@ jobs:
if [ "${{ secrets.TokenName5}}" ] ; then if [ "${{ secrets.TokenName5}}" ] ; then
echo "${{ secrets.TokenName5}}=${{ secrets.TokenValue5}}" >> env.list echo "${{ secrets.TokenName5}}=${{ secrets.TokenValue5}}" >> env.list
fi fi
echo "TEST_DNS_NO_WILDCARD" >> env.list echo "TEST_DNS_NO_WILDCARD" >> env.list
echo "TEST_DNS_SLEEP" >> env.list echo "TEST_DNS_SLEEP" >> env.list
- name: Run acmetest - name: Run acmetest
run: cd ../acmetest && ./rundocker.sh testall run: cd ../acmetest && ./rundocker.sh testall
@ -226,8 +226,10 @@ jobs:
- uses: vmactions/solaris-vm@v0.0.3 - uses: vmactions/solaris-vm@v0.0.3
with: with:
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: pkgutil -y -i socat curl prepare: pkgutil -y -i socat
run: | run: |
pkg set-mediator -v -I default@1.1 openssl
export PATH=/usr/gnu/bin:$PATH
if [ "${{ secrets.TokenName1}}" ] ; then if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}=${{ secrets.TokenValue1}} export ${{ secrets.TokenName1}}=${{ secrets.TokenValue1}}
fi fi
@ -245,5 +247,3 @@ jobs:
fi fi
cd ../acmetest cd ../acmetest
./letest.sh ./letest.sh

246
dnsapi/dns_oci.sh Normal file
View File

@ -0,0 +1,246 @@
#!/usr/bin/env sh
#
# Acme.sh DNS API plugin for Oracle Cloud Infrastructure
# Copyright (c) 2021, Oracle and/or its affiliates
#
# Required environment variables:
# - OCI_TENANCY : OCID of tenancy that contains the target DNS zone
# - OCI_USER : OCID of user with permission to add/remove records from zones
# - OCI_FINGERPRINT: fingerprint of the public key for the user
# - OCI_PRIVATE_KEY: Path to private API signing key file in PEM format
#
# Optional environment variables:
# - OCI_KEY_PASSPHRASE: if the private key above s encrypted, the passphrase is required
# - OCI_REGION: Your home region will probably response the fastest
#
dns_oci_add() {
_fqdn="$1"
_rdata="$2"
if _oci_config; then
if ! _get_zone "$_fqdn"; then
_err "Error: DNS Zone not found for $_fqdn."
return 1
fi
if [ "$_sub_domain" ] && [ "$_domain" ]; then
_add_record_body="{\"items\":[{\"domain\":\"${_sub_domain}.${_domain}\",\"rdata\":\"$_rdata\",\"rtype\":\"TXT\",\"ttl\": 30,\"operation\":\"ADD\"}]}"
response=$(_signed_request "PATCH" "/20180115/zones/${_domain}/records" "$_add_record_body")
if [ "$response" ]; then
_info "Success: added TXT record for ${_sub_domain}.${_domain}."
else
_err "Error: failed to add TXT record for ${_sub_domain}.${_domain}."
return 1
fi
fi
else
return 1
fi
}
dns_oci_rm() {
_fqdn="$1"
_rdata="$2"
if _oci_config; then
if ! _get_zone "$_fqdn"; then
_err "Error: DNS Zone not found for $_fqdn."
return 1
fi
if [ "$_sub_domain" ] && [ "$_domain" ]; then
_remove_record_body="{\"items\":[{\"domain\":\"${_sub_domain}.${_domain}\",\"rdata\":\"$_rdata\",\"rtype\":\"TXT\",\"operation\":\"REMOVE\"}]}"
response=$(_signed_request "PATCH" "/20180115/zones/${_domain}/records" "$_remove_record_body")
if [ "$response" ]; then
_info "Success: removed TXT record for ${_sub_domain}.${_domain}."
else
_err "Error: failed to remove TXT record for ${_sub_domain}.${_domain}."
return 1
fi
fi
else
return 1
fi
}
#################### Private functions below ##################################
_oci_config() {
OCI_TENANCY="${OCI_TENANCY:-$(_readaccountconf_mutable OCI_TENANCY)}"
OCI_USER="${OCI_USER:-$(_readaccountconf_mutable OCI_USER)}"
OCI_FINGERPRINT="${OCI_FINGERPRINT:-$(_readaccountconf_mutable OCI_FINGERPRINT)}"
OCI_PRIVATE_KEY="${OCI_PRIVATE_KEY:-$(_readaccountconf_mutable OCI_PRIVATE_KEY)}"
OCI_KEY_PASSPHRASE="${OCI_KEY_PASSPHRASE:-$(_readaccountconf_mutable OCI_KEY_PASSPHRASE)}"
OCI_REGION="${OCI_REGION:-$(_readaccountconf_mutable OCI_REGION)}"
_not_set=""
_ret=0
if [ -f "$OCI_PRIVATE_KEY" ]; then
OCI_PRIVATE_KEY="$(openssl enc -a -A <"$OCI_PRIVATE_KEY")"
fi
if [ -z "$OCI_TENANCY" ]; then
_not_set="OCI_TENANCY "
fi
if [ -z "$OCI_USER" ]; then
_not_set="${_not_set}OCI_USER "
fi
if [ -z "$OCI_FINGERPRINT" ]; then
_not_set="${_not_set}OCI_FINGERPRINT "
fi
if [ -z "$OCI_PRIVATE_KEY" ]; then
_not_set="${_not_set}OCI_PRIVATE_KEY"
fi
if [ "$_not_set" ]; then
_err "Fatal: environment variable(s): ${_not_set} not set."
_ret=1
else
_saveaccountconf_mutable OCI_TENANCY "$OCI_TENANCY"
_saveaccountconf_mutable OCI_USER "$OCI_USER"
_saveaccountconf_mutable OCI_FINGERPRINT "$OCI_FINGERPRINT"
_saveaccountconf_mutable OCI_PRIVATE_KEY "$OCI_PRIVATE_KEY"
fi
if [ "$OCI_PRIVATE_KEY" ] && [ "$(printf "%s\n" "$OCI_PRIVATE_KEY" | wc -l)" -eq 1 ]; then
OCI_PRIVATE_KEY="$(echo "$OCI_PRIVATE_KEY" | openssl enc -d -a -A)"
_secure_debug3 OCI_PRIVATE_KEY "$OCI_PRIVATE_KEY"
fi
if [ "$OCI_KEY_PASSPHRASE" ]; then
_saveaccountconf_mutable OCI_KEY_PASSPHRASE "$OCI_KEY_PASSPHRASE"
fi
if [ "$OCI_REGION" ]; then
_saveaccountconf_mutable OCI_REGION "$OCI_REGION"
else
OCI_REGION="us-ashburn-1"
fi
return $_ret
}
# _get_zone(): retrieves the Zone name and OCID
#
# _sub_domain=_acme-challenge.www
# _domain=domain.com
# _domain_ociid=ocid1.dns-zone.oc1..
_get_zone() {
domain=$1
i=1
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f $i-100)
_debug h "$h"
if [ -z "$h" ]; then
# not valid
return 1
fi
_domain_id=$(_signed_request "GET" "/20180115/zones/$h" "" "id")
if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
_domain=$h
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
return 1
}
_signed_request() {
_sig_method="$1"
_sig_target="$2"
_sig_body="$3"
_return_field="$4"
_sig_host="dns.$OCI_REGION.oraclecloud.com"
_sig_keyId="$OCI_TENANCY/$OCI_USER/$OCI_FINGERPRINT"
_sig_alg="rsa-sha256"
_sig_version="1"
_sig_now="$(LC_ALL=C \date -u "+%a, %d %h %Y %H:%M:%S GMT")"
if [ "$OCI_KEY_PASSPHRASE" ]; then
export OCI_KEY_PASSPHRASE="$OCI_KEY_PASSPHRASE"
_sig_passinArg="-passin env:OCI_KEY_PASSPHRASE"
fi
_request_method=$(printf %s "$_sig_method" | _lower_case)
_curl_method=$(printf %s "$_sig_method" | _upper_case)
_request_target="(request-target): $_request_method $_sig_target"
_date_header="date: $_sig_now"
_host_header="host: $_sig_host"
_string_to_sign="$_request_target\n$_date_header\n$_host_header"
_sig_headers="(request-target) date host"
if [ "$_sig_body" ]; then
_secure_debug3 _sig_body "$_sig_body"
_sig_body_sha256="x-content-sha256: $(printf %s "$_sig_body" | openssl dgst -binary -sha256 | openssl enc -e -base64)"
_sig_body_type="content-type: application/json"
_sig_body_length="content-length: ${#_sig_body}"
_string_to_sign="$_string_to_sign\n$_sig_body_sha256\n$_sig_body_type\n$_sig_body_length"
_sig_headers="$_sig_headers x-content-sha256 content-type content-length"
fi
_tmp_file=$(_mktemp)
if [ -f "$_tmp_file" ]; then
printf '%s' "$OCI_PRIVATE_KEY" >"$_tmp_file"
# Double quoting the file and passphrase breaks openssl
# shellcheck disable=SC2086
_signature=$(printf '%b' "$_string_to_sign" | openssl dgst -sha256 -sign $_tmp_file $_sig_passinArg | openssl enc -e -base64 | tr -d '\r\n')
rm -f "$_tmp_file"
fi
_signed_header="Authorization: Signature version=\"$_sig_version\",keyId=\"$_sig_keyId\",algorithm=\"$_sig_alg\",headers=\"$_sig_headers\",signature=\"$_signature\""
_secure_debug3 _signed_header "$_signed_header"
if [ "$_curl_method" = "GET" ]; then
export _H1="$_date_header"
export _H2="$_signed_header"
_response="$(_get "https://${_sig_host}${_sig_target}")"
elif [ "$_curl_method" = "PATCH" ]; then
export _H1="$_date_header"
export _H2="$_sig_body_sha256"
export _H3="$_sig_body_type"
export _H4="$_sig_body_length"
export _H5="$_signed_header"
_response="$(_post "$_sig_body" "https://${_sig_host}${_sig_target}" "" "PATCH")"
else
_err "Unable to process method: $_curl_method."
fi
_ret="$?"
if [ "$_return_field" ]; then
_response="$(echo "$_response" | sed 's/\\\"//g'))"
_return=$(echo "${_response}" | _egrep_o "\"$_return_field\"\\s*:\\s*\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d "\"")
else
_return="$_response"
fi
printf "%s" "$_return"
return $_ret
}