]> git.ipfire.org Git - thirdparty/cups.git/commitdiff
Add OAuth and X.509 utilities (Issue #1184)
authorMichael R Sweet <msweet@msweet.org>
Tue, 4 Mar 2025 18:43:37 +0000 (13:43 -0500)
committerMichael R Sweet <msweet@msweet.org>
Tue, 4 Mar 2025 18:43:37 +0000 (13:43 -0500)
13 files changed:
.gitignore
CHANGES.md
cups/oauth.c
doc/Makefile
doc/help/man-cups-oauth.html [new file with mode: 0644]
doc/help/man-cups-x509.html [new file with mode: 0644]
man/Makefile
man/cups-oauth.1 [new file with mode: 0644]
man/cups-x509.1 [new file with mode: 0644]
tools/Dependencies
tools/Makefile
tools/cups-oauth.c [new file with mode: 0644]
tools/cups-x509.c [new file with mode: 0644]

index 1780afd5b7d134081d1b5971a3ed9b54f53e817b..81bc22ca2ebef03c20fddd38048e189dc7aba60f 100644 (file)
@@ -35,6 +35,7 @@
 /conf/mime.convs
 /conf/pam.std
 /conf/snmp.conf
+/cups/.testssl
 /cups/cachebench
 /cups/fuzzipp
 /cups/libcups.dylib
 /templates/*/header.tmpl
 /test/cups-str-*.html
 /test/*_log-*
+/tools/cups-oauth
+/tools/cups-x509
 /tools/ippevepcl
 /tools/ippeveprinter
 /tools/ippeveprinter-static
index afe7a30b6165f66777bc4f3a4a3324562723a046..8fe45e6983060f2fc72810ccd8954dd183c978fe 100644 (file)
@@ -34,6 +34,7 @@ Changes in CUPS v2.5b1 (YYYY-MM-DD)
 - Added support for specifying permissions with the `cupsFileOpen` API.
 - Added new `cupsGetClock` API.
 - Added new `cupsParseOptions2` API with "end" argument.
+- Added `cups-oauth` and `cups-x509` utilities (Issue #1184)
 - Updated documentation (Issue #984, Issue #1086)
 - Updated the configure script to default to installing to /usr/local.
 - Updated CUPS to require TLS support - OpenSSL, GNUTLS and LibreSSL are
index 5ed0cf35755e26daa88ec509952162035029b7e7..a1e49957df52d7fd7c3667a5f418da57264a91d4 100644 (file)
@@ -305,15 +305,15 @@ cupsOAuthCopyUserId(
 // names to request during authorization.  The list of supported scope names are
 // available from the Authorization Server metadata, for example:
 //
-// The "redirect_uri" parameter specifies a 'http:' URL with a listen address,
-// port, and path to use.  If `NULL`, 127.0.0.1 on a random port is used with a
-// path of "/".
-//
 // ```
 // cups_json_t *metadata = cupsOAuthGetMetadata(auth_uri);
 // cups_json_t *scopes_supported = cupsJSONFind(metadata, "scopes_supported");
 // ```
 //
+// The "redirect_uri" parameter specifies a 'http:' URL with a listen address,
+// port, and path to use.  If `NULL`, 127.0.0.1 on a random port is used with a
+// path of "/".
+//
 // The returned authorization code must be freed using the `free` function.
 //
 
index fab908857f29dbdb178f97305a4821ed80561a32..38f2b4ce3bf77ae07c812bc38a8debd39400aeb2 100644 (file)
@@ -53,7 +53,9 @@ HELPFILES     =       \
                        help/man-cups.html \
                        help/man-cups-files.conf.html \
                        help/man-cups-lpd.html \
+                       help/man-cups-oauth.html \
                        help/man-cups-snmp.html \
+                       help/man-cups-x509.html \
                        help/man-cupsaccept.html \
                        help/man-cupsd.conf.html \
                        help/man-cupsd.html \
diff --git a/doc/help/man-cups-oauth.html b/doc/help/man-cups-oauth.html
new file mode 100644 (file)
index 0000000..f95acb5
--- /dev/null
@@ -0,0 +1,162 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta name="creator" content="mantohtml v2.0.2">
+    <title>cups-oauth(1)</title>
+  </head>
+  <body>
+    <h1 id="cups-oauth-1">cups-oauth(1)</h1>
+    <h2 id="cups-oauth-1.name">Name</h2>
+<p>cups-oauth - interact with an oauth/openid authorization server
+</p>
+    <h2 id="cups-oauth-1.synopsis">Synopsis</h2>
+<p><strong>cups-oauth</strong>
+<strong>--help</strong>
+<br>
+<strong>cups-oauth</strong>
+<strong>--version</strong>
+<br>
+<strong>cups-oauth</strong>
+[
+<strong>-a</strong>
+<em>OAUTH-URI</em>
+] [
+<strong>-s</strong>
+<em>SCOPE(S)</em>
+]
+<strong>authorize</strong>
+<em>[RESOURCE]</em>
+<br>
+<strong>cups-oauth</strong>
+[
+<strong>-a</strong>
+<em>OAUTH-URI</em>
+]
+<strong>clear</strong>
+<em>[RESOURCE]</em>
+<br>
+<strong>cups-oauth</strong>
+[
+<strong>-a</strong>
+<em>OAUTH-URI</em>
+]
+<strong>get-access-token</strong>
+<em>[RESOURCE]</em>
+<br>
+<strong>cups-oauth</strong>
+[
+<strong>-a</strong>
+<em>OAUTH-URI</em>
+]
+<strong>get-client-id</strong>
+<br>
+<strong>cups-oauth</strong>
+[
+<strong>-a</strong>
+<em>OAUTH-URI</em>
+]
+<strong>get-metadata</strong>
+<em>[NAME]</em>
+<br>
+<strong>cups-oauth</strong>
+[
+<strong>-a</strong>
+<em>OAUTH-URI</em>
+]
+<strong>get-user-id</strong>
+<em>[RESOURCE]</em>
+<em>[NAME]</em>
+<br>
+<strong>cups-oauth</strong>
+[
+<strong>-a</strong>
+<em>OAUTH-URI</em>
+]
+<strong>set-access-token</strong>
+<em>[RESOURCE]</em>
+<em>TOKEN</em>
+<br>
+<strong>cups-oauth</strong>
+[
+<strong>-a</strong>
+<em>OAUTH-URI</em>
+]
+<strong>set-client-data</strong>
+<em>CLIENT-ID</em>
+<em>CLIENT-SECRET</em>
+</p>
+    <h2 id="cups-oauth-1.description">Description</h2>
+<p>The
+<strong>cups-oauth</strong>
+utility interacts with an OAuth/OpenID authorization server.
+Authorizations are often linked to a resource (a printer URI, web page URL, etc.)
+</p>
+    <h2 id="cups-oauth-1.options">Options</h2>
+<p>The following options are recognized by
+<strong>cups-oauth:</strong>
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>--help</strong><br>
+Show program usage.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>--version</strong><br>
+Show the CUPS version.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-a </strong><em>OAUTH-URI</em><br>
+Specifies the OAuth/OpenID authorization server URL.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-s </strong><em>SCOPE(S)</em><br>
+Specifies a space-delimited list of scope names to use when authorizing access.
+The default is to request authorization for all supported scopes.
+</p>
+    <h2 id="cups-oauth-1.commands">Commands</h2>
+    <h3 id="cups-oauth-1.commands.authorize">Authorize</h3>
+<p>Starts an authorization workflow with the default web browser.
+If a resource URI is specified, the authorization is specific to that resource.
+The access token is send to the standard output on success.
+</p>
+    <h3 id="cups-oauth-1.commands.clear">Clear</h3>
+<p>Clears any authorization for the specified resource or for all resources if no resource URI is supplied.
+</p>
+    <h3 id="cups-oauth-1.commands.get-access-token">Get-Access-Token</h3>
+<p>Output the current, unexpired access token, if any, to the standard output.
+</p>
+    <h3 id="cups-oauth-1.commands.get-client-id">Get-Client-Id</h3>
+<p>Output the client ID string, if any, to the standard output.
+</p>
+    <h3 id="cups-oauth-1.commands.get-metadata">Get-Metadata</h3>
+<p>Get the OAuth/OpenID authorization server metadata and send it to the standard output.
+If a name is specified, the output is just the value for the specified metadata.
+</p>
+    <h3 id="cups-oauth-1.commands.get-user-id">Get-User-Id</h3>
+<p>Get the OpenID user ID information and send it to the standard output.
+If a name is specified, the output is just the named claim from the user ID.
+</p>
+    <h3 id="cups-oauth-1.commands.set-access-token">Set-Access-Token</h3>
+<p>Set the access token (which is sometimes also called an API key) for the specified resource or for all resources.
+</p>
+    <h3 id="cups-oauth-1.commands.set-client-data">Set-Client-Data</h3>
+<p>Set the client ID string and secret for an OAuth/OpenID authorization server.
+</p>
+    <h2 id="cups-oauth-1.environment-variables">Environment Variables</h2>
+<p>The
+<strong>CUPS_OAUTH_URI</strong>
+environment variable sets the default OAuth/OpenID authorization server URL.
+</p>
+    <p>The
+<strong>CUPS_OAUTH_SCOPES</strong>
+environment variable sets the default OAuth/OpenID scopes as a space-delimited list.
+</p>
+    <h2 id="cups-oauth-1.notes">Notes</h2>
+<p>CUPS uses a redirect URI of &quot;<a href="http://127.0.0.1/&quot;">http://127.0.0.1/&quot;</a> for all authorization on the local system.
+</p>
+    <h2 id="cups-oauth-1.examples">Examples</h2>
+<p>TBD
+</p>
+    <h2 id="cups-oauth-1.see-also">See Also</h2>
+<a href="cups.html"><p><strong>cups</strong>(1)</a>
+
+</p>
+    <h2 id="cups-oauth-1.copyright">Copyright</h2>
+<p>Copyright &copy; 2025 by OpenPrinting.
+  </body>
+</html>
diff --git a/doc/help/man-cups-x509.html b/doc/help/man-cups-x509.html
new file mode 100644 (file)
index 0000000..8d0017c
--- /dev/null
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta name="creator" content="mantohtml v2.0.2">
+    <title>cups-x509(1)</title>
+  </head>
+  <body>
+    <h1 id="cups-x509-1">cups-x509(1)</h1>
+    <h2 id="cups-x509-1.name">Name</h2>
+<p>cups-x509 - description
+</p>
+    <h2 id="cups-x509-1.synopsis">Synopsis</h2>
+<p><strong>cups-x509</strong>
+<strong>--help</strong>
+<br>
+<strong>cups-x509</strong>
+<strong>--version</strong>
+<br>
+<strong>cups-x509</strong>
+[
+<strong>--pin</strong>
+] [
+<strong>--require-ca</strong>
+] [
+<strong>-C</strong>
+<em>COUNTRY</em>
+] [
+<strong>-L</strong>
+<em>LOCALITY</em>
+] [
+<strong>-O</strong>
+<em>ORGANIZATION</em>
+] [
+<strong>-R</strong>
+<em>CSR-FILENAME</em>
+] [
+<strong>-S</strong>
+<em>STATE-PROVINCE</em>
+] [
+<strong>-U</strong>
+<em>ORGANIZATIONAL-UNIT</em>
+] [
+<strong>-a</strong>
+<em>SUBJECT-ALT-NAME</em>
+] [
+<strong>-d</strong>
+<em>DAYS</em>
+] [
+<strong>-p</strong>
+<em>PURPOSE</em>
+] [
+<strong>-r</strong>
+<em>ROOT-NAME</em>
+] [
+<strong>-t</strong>
+<em>TYPE</em>
+] [
+<strong>-u</strong>
+<em>USAGE</em>
+]
+<em>COMMAND</em>
+<em>[ARGUMENT(S)]</em>
+</p>
+    <h2 id="cups-x509-1.description">Description</h2>
+<p>The
+<strong>cups-x509</strong>
+utility manages X.509 certificates and certificate requests, and supports client and server tests.
+</p>
+    <h2 id="cups-x509-1.options">Options</h2>
+<p>The following options are recognized by
+<strong>cups-x509:</strong>
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>--help</strong><br>
+Show program usage.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>--pin</strong><br>
+Pin the server's X.509 certificate found by the client command.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>--require-ca</strong><br>
+Require the server's X.509 certificate found by the client command to be signed by a known CA.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>--version</strong><br>
+Show the CUPS version.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-C </strong><em>COUNTRY</em><br>
+Specify the country for new X.509 certificates and certificate requests.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-L </strong><em>LOCALITY</em><br>
+Specify the locality (city, town, etc.) for new X.509 certificates and certificate requests.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-O </strong><em>ORGANIZATION</em><br>
+Specify the organization name for new X.509 certificates and certificate requests.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-R </strong><em>CSR-FILENAME</em><br>
+Specify an X.509 certificate signing request in PEM format to be used when signing a certificate with the
+<strong>ca</strong>
+command.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-S </strong><em>STATE-PROVINCE</em><br>
+Specify the state/province name for new X.509 certificates and certificate requests.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-U </strong><em>ORGANIZATIONAL-UNIT</em><br>
+Specify the organizational unit name for new X.509 certificates and certificate requests.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-a </strong><em>SUBJECT-ALT-NAME</em><br>
+Specify an alternate name for new X.509 certificates and certificate requests.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-d </strong><em>DAYS</em><br>
+Specify the number of days before a new X.509 certificate will expire.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-p </strong><em>PURPOSE</em><br>
+Specify the purpose of the X.509 certificate or certificate request as a comma-delimited list of purposes.
+The supported purposes are &quot;serverAuth&quot; for TLS server authentication, &quot;clientAuth&quot; for TLS client authentication, &quot;codeSigning&quot; for executable code signing, &quot;emailProtection&quot; for S/MIME encryption and signing, &quot;timeStamping&quot; for secure timestamps, and &quot;OCSPSigning&quot; for Online Certificate Status Protocol services.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-r </strong><em>ROOT-NAME</em><br>
+Specify the common name of the X.509 root certificate to use.
+The default root certificate is named &quot;_site_&quot;.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-t </strong><em>TYPE</em><br>
+Specify the certificate type - &quot;rsa-2048&quot; for 2048-bit RSA, &quot;rsa-3072&quot; for 3072-bit RSA, &quot;rsa-4096&quot; for 4096-bit RSA, &quot;ecdsa-p256&quot; for 256-bit ECDSA, &quot;ecdsa-p384&quot; for 384-bit ECDSA, or &quot;ecdsa-p521&quot; for 521-bit ECDSA.
+</p>
+    <p style="margin-left: 2.5em; text-indent: -2.5em;"><strong>-u </strong><em>USAGE</em><br>
+Specify the usage for the certificate as a comma-delimited list of uses.
+The supported uses are &quot;digitalSignature&quot;, &quot;nonRepudiation&quot;, &quot;keyEncipherment&quot;, &quot;dataEncipherment&quot;, &quot;keyAgreement&quot;, &quot;keyCertSign&quot;, &quot;cRLSign&quot;, &quot;encipherOnly&quot;, and  &quot;decipherOnly&quot;.
+The preset &quot;default-ca&quot; specifies those uses required for a Certificate Authority, and the preset &quot;default-tls&quot; specifies those uses required for TLS.
+</p>
+    <h2 id="cups-x509-1.commands">Commands</h2>
+    <h3 id="cups-x509-1.commands.ca-common-name">Ca Common-Name</h3>
+<p>Sign a certificate request for the specified common name.
+</p>
+    <h3 id="cups-x509-1.commands.cacert-common-name">Cacert Common-Name</h3>
+<p>Create a CA certificate for the specified common name.
+</p>
+    <h3 id="cups-x509-1.commands.cert-common-name">Cert Common-Name</h3>
+<p>Create a certificate for the specified common name.
+</p>
+    <h3 id="cups-x509-1.commands.client-uri">Client Uri</h3>
+<p>Connect to the specified URI and validate the server's certificate.
+</p>
+    <h3 id="cups-x509-1.commands.csr-common-name">Csr Common-Name</h3>
+<p>Create a certificate signing request for the specified common name.
+</p>
+    <h3 id="cups-x509-1.commands.server-common-nameport">Server Common-Name[:Port]</h3>
+<p>Run a HTTPS test server that echos back the resource path for every GET request.
+If PORT is not specified, uses a port number from 8000 to 8999.
+</p>
+    <h3 id="cups-x509-1.commands.show-common-name">Show Common-Name</h3>
+<p>Shows any stored credentials for the specified common name.
+</p>
+    <h2 id="cups-x509-1.examples">Examples</h2>
+<p>TBD
+</p>
+    <h2 id="cups-x509-1.see-also">See Also</h2>
+<a href="cups.html"><p><strong>cups</strong>(1)</a>
+
+</p>
+    <h2 id="cups-x509-1.copyright">Copyright</h2>
+<p>Copyright &copy; 2025 by OpenPrinting.
+  </body>
+</html>
index 83a0f6d5df4c9fbf502cb9fbd8fcc87927cbe742..0dc8b07ad37c7eba480b2847107bb6c4b4bccc4d 100644 (file)
@@ -4,8 +4,6 @@
 # The "html" target depends on "mantohtml" from https://www.msweet.org/mantohtml
 #
 # Copyright © 2020-2025 by OpenPrinting.
-# Copyright © 2007-2019 by Apple Inc.
-# Copyright © 1993-2006 by Easy Software Products.
 #
 # Licensed under Apache License v2.0.  See the file "LICENSE" for more
 # information.
@@ -20,6 +18,8 @@ include ../Makedefs
 
 MAN1   =       cancel.1 \
                cups.1 \
+               cups-oauth.1 \
+               cups-x509.1 \
                cupstestppd.1 \
                ippeveprinter.1 \
                $(IPPFIND_MAN) \
diff --git a/man/cups-oauth.1 b/man/cups-oauth.1
new file mode 100644 (file)
index 0000000..7a7c094
--- /dev/null
@@ -0,0 +1,144 @@
+.\"
+.\" cups-oauth man page for CUPS.
+.\"
+.\" Copyright © 2025 by OpenPrinting.
+.\"
+.\" Licensed under Apache License v2.0.  See the file "LICENSE" for more
+.\" information.
+.\"
+.TH cups-oauth 1 "CUPS" "2025-03-04" "OpenPrinting"
+.SH NAME
+cups-oauth \- interact with an oauth/openid authorization server
+.SH SYNOPSIS
+.B cups-oauth
+.B \-\-help
+.br
+.B cups-oauth
+.B \-\-version
+.br
+.B cups-oauth
+[
+.B \-a
+.I OAUTH-URI
+] [
+.B \-s
+.I SCOPE(S)
+]
+.B authorize
+.I [RESOURCE]
+.br
+.B cups-oauth
+[
+.B \-a
+.I OAUTH-URI
+]
+.B clear
+.I [RESOURCE]
+.br
+.B cups-oauth
+[
+.B \-a
+.I OAUTH-URI
+]
+.B get-access-token
+.I [RESOURCE]
+.br
+.B cups-oauth
+[
+.B \-a
+.I OAUTH-URI
+]
+.B get-client-id
+.br
+.B cups-oauth
+[
+.B \-a
+.I OAUTH-URI
+]
+.B get-metadata
+.I [NAME]
+.br
+.B cups-oauth
+[
+.B \-a
+.I OAUTH-URI
+]
+.B get-user-id
+.I [RESOURCE]
+.I [NAME]
+.br
+.B cups-oauth
+[
+.B \-a
+.I OAUTH-URI
+]
+.B set-access-token
+.I [RESOURCE]
+.I TOKEN
+.br
+.B cups-oauth
+[
+.B \-a
+.I OAUTH-URI
+]
+.B set-client-data
+.I CLIENT-ID
+.I CLIENT-SECRET
+.SH DESCRIPTION
+The
+.B cups-oauth
+utility interacts with an OAuth/OpenID authorization server.
+Authorizations are often linked to a resource (a printer URI, web page URL, etc.)
+.SH OPTIONS
+The following options are recognized by
+.B cups-oauth:
+.TP 5
+.B \-\-help
+Show program usage.
+.TP 5
+.B \-\-version
+Show the CUPS version.
+.TP 5
+\fB\-a \fIOAUTH-URI\fR
+Specifies the OAuth/OpenID authorization server URL.
+.TP 5
+\fB\-s \fISCOPE(S)\fR
+Specifies a space-delimited list of scope names to use when authorizing access.
+The default is to request authorization for all supported scopes.
+.SH COMMANDS
+.SS authorize
+Starts an authorization workflow with the default web browser.
+If a resource URI is specified, the authorization is specific to that resource.
+The access token is send to the standard output on success.
+.SS clear
+Clears any authorization for the specified resource or for all resources if no resource URI is supplied.
+.SS get-access-token
+Output the current, unexpired access token, if any, to the standard output.
+.SS get-client-id
+Output the client ID string, if any, to the standard output.
+.SS get-metadata
+Get the OAuth/OpenID authorization server metadata and send it to the standard output.
+If a name is specified, the output is just the value for the specified metadata.
+.SS get-user-id
+Get the OpenID user ID information and send it to the standard output.
+If a name is specified, the output is just the named claim from the user ID.
+.SS set-access-token
+Set the access token (which is sometimes also called an API key) for the specified resource or for all resources.
+.SS set-client-data
+Set the client ID string and secret for an OAuth/OpenID authorization server.
+.SH ENVIRONMENT VARIABLES
+The
+.B CUPS_OAUTH_URI
+environment variable sets the default OAuth/OpenID authorization server URL.
+.PP
+The
+.B CUPS_OAUTH_SCOPES
+environment variable sets the default OAuth/OpenID scopes as a space-delimited list.
+.SH NOTES
+CUPS uses a redirect URI of "http://127.0.0.1/" for all authorization on the local system.
+.SH EXAMPLES
+TBD
+.SH SEE ALSO
+.BR cups (1)
+.SH COPYRIGHT
+Copyright \[co] 2025 by OpenPrinting.
diff --git a/man/cups-x509.1 b/man/cups-x509.1
new file mode 100644 (file)
index 0000000..2f42ab7
--- /dev/null
@@ -0,0 +1,145 @@
+.\"
+.\" cups-x509 man page for CUPS.
+.\"
+.\" Copyright © 2025 by OpenPrinting.
+.\"
+.\" Licensed under Apache License v2.0.  See the file "LICENSE" for more
+.\" information.
+.\"
+.TH cups-x509 1 "CUPS" "2025-03-04" "OpenPrinting"
+.SH NAME
+cups-x509 \- description
+.SH SYNOPSIS
+.B cups-x509
+.B \-\-help
+.br
+.B cups-x509
+.B \-\-version
+.br
+.B cups-x509
+[
+.B \-\-pin
+] [
+.B \-\-require\-ca
+] [
+.B \-C
+.I COUNTRY
+] [
+.B \-L
+.I LOCALITY
+] [
+.B \-O
+.I ORGANIZATION
+] [
+.B \-R
+.I CSR-FILENAME
+] [
+.B \-S
+.I STATE-PROVINCE
+] [
+.B \-U
+.I ORGANIZATIONAL-UNIT
+] [
+.B \-a
+.I SUBJECT-ALT-NAME
+] [
+.B \-d
+.I DAYS
+] [
+.B \-p
+.I PURPOSE
+] [
+.B \-r
+.I ROOT-NAME
+] [
+.B \-t
+.I TYPE
+] [
+.B \-u
+.I USAGE
+]
+.I COMMAND
+.I [ARGUMENT(S)]
+.SH DESCRIPTION
+The
+.B cups-x509
+utility manages X.509 certificates and certificate requests, and supports client and server tests.
+.SH OPTIONS
+The following options are recognized by
+.B cups-x509:
+.TP 5
+.B \-\-help
+Show program usage.
+.TP 5
+.B \-\-pin
+Pin the server's X.509 certificate found by the client command.
+.TP 5
+.B \-\-require\-ca
+Require the server's X.509 certificate found by the client command to be signed by a known CA.
+.TP 5
+.B \-\-version
+Show the CUPS version.
+.TP 5
+\fB-C \fICOUNTRY\fR
+Specify the country for new X.509 certificates and certificate requests.
+.TP 5
+\fB-L \fILOCALITY\fR
+Specify the locality (city, town, etc.) for new X.509 certificates and certificate requests.
+.TP 5
+\fB-O \fIORGANIZATION\fR
+Specify the organization name for new X.509 certificates and certificate requests.
+.TP 5
+\fB-R \fICSR-FILENAME\fR
+Specify an X.509 certificate signing request in PEM format to be used when signing a certificate with the
+.B ca
+command.
+.TP 5
+\fB-S \fISTATE-PROVINCE\fR
+Specify the state/province name for new X.509 certificates and certificate requests.
+.TP 5
+\fB-U \fIORGANIZATIONAL-UNIT\fR
+Specify the organizational unit name for new X.509 certificates and certificate requests.
+.TP 5
+\fB-a \fISUBJECT-ALT-NAME\fR
+Specify an alternate name for new X.509 certificates and certificate requests.
+.TP 5
+\fB-d \fIDAYS\fR
+Specify the number of days before a new X.509 certificate will expire.
+.TP 5
+\fB-p \fIPURPOSE\fR
+Specify the purpose of the X.509 certificate or certificate request as a comma-delimited list of purposes.
+The supported purposes are "serverAuth" for TLS server authentication, "clientAuth" for TLS client authentication, "codeSigning" for executable code signing, "emailProtection" for S/MIME encryption and signing, "timeStamping" for secure timestamps, and "OCSPSigning" for Online Certificate Status Protocol services.
+.TP 5
+\fB-r \fIROOT-NAME\fR
+Specify the common name of the X.509 root certificate to use.
+The default root certificate is named "_site_".
+.TP 5
+\fB-t \fITYPE\fR
+Specify the certificate type - "rsa-2048" for 2048-bit RSA, "rsa-3072" for 3072-bit RSA, "rsa-4096" for 4096-bit RSA, "ecdsa-p256" for 256-bit ECDSA, "ecdsa-p384" for 384-bit ECDSA, or "ecdsa-p521" for 521-bit ECDSA.
+.TP 5
+\fB-u \fIUSAGE\fR
+Specify the usage for the certificate as a comma-delimited list of uses.
+The supported uses are "digitalSignature", "nonRepudiation", "keyEncipherment", "dataEncipherment", "keyAgreement", "keyCertSign", "cRLSign", "encipherOnly", and  "decipherOnly".
+The preset "default-ca" specifies those uses required for a Certificate Authority, and the preset "default-tls" specifies those uses required for TLS.
+.SH COMMANDS
+.SS ca COMMON-NAME
+Sign a certificate request for the specified common name.
+.SS cacert COMMON-NAME
+Create a CA certificate for the specified common name.
+.SS cert COMMON-NAME
+Create a certificate for the specified common name.
+.SS client URI
+Connect to the specified URI and validate the server's certificate.
+.SS csr COMMON-NAME
+Create a certificate signing request for the specified common name.
+.SS server COMMON-NAME[:PORT]
+Run a HTTPS test server that echos back the resource path for every GET request.
+If PORT is not specified, uses a port number from 8000 to 8999.
+.SS show COMMON-NAME
+Shows any stored credentials for the specified common name.
+.SH EXAMPLES
+TBD
+.SH SEE ALSO
+.BR cups (1)
+.SH COPYRIGHT
+Copyright \[co] 2025 by OpenPrinting.
index c9194b6dc360c5776678206a960a2d94cc0ae421..94ed960b22f4c47e0208f143faf4dad76f71f52e 100644 (file)
@@ -1,3 +1,54 @@
+cups-oauth.o: cups-oauth.c ../cups/cups-private.h \
+  ../cups/string-private.h ../config.h ../cups/base.h \
+  ../cups/debug-internal.h ../cups/debug-private.h ../cups/ipp-private.h \
+  ../cups/cups.h ../cups/file.h ../cups/ipp.h ../cups/http.h \
+  ../cups/array.h ../cups/language.h ../cups/pwg.h \
+  ../cups/http-private.h \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  ../cups/language-private.h \
+  ../cups/transcode.h ../cups/pwg-private.h ../cups/thread.h \
+  ../cups/oauth.h ../cups/jwt.h ../cups/json.h
+cups-x509.o: cups-x509.c ../cups/cups-private.h ../cups/string-private.h \
+  ../config.h ../cups/base.h ../cups/debug-internal.h \
+  ../cups/debug-private.h ../cups/ipp-private.h ../cups/cups.h \
+  ../cups/file.h ../cups/ipp.h ../cups/http.h ../cups/array.h \
+  ../cups/language.h ../cups/pwg.h ../cups/http-private.h \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  \
+  ../cups/language-private.h \
+  ../cups/transcode.h ../cups/pwg-private.h ../cups/thread.h
 ippevepcl.o: ippevepcl.c ippevecommon.h ../cups/cups.h ../cups/file.h \
   ../cups/base.h ../cups/ipp.h ../cups/http.h ../cups/array.h \
   ../cups/language.h ../cups/pwg.h ../cups/raster.h \
@@ -25,12 +76,9 @@ ippeveprinter.o: ippeveprinter.c ../cups/cups-private.h \
   \
   \
   \
-  \
-  \
-  \
-  ../cups/language-private.h ../cups/transcode.h ../cups/pwg-private.h \
-  ../cups/thread.h ../cups/dnssd.h printer-png.h printer-lg-png.h \
-  printer-sm-png.h
+  ../cups/language-private.h \
+  ../cups/transcode.h ../cups/pwg-private.h ../cups/thread.h \
+  ../cups/dnssd.h printer-png.h printer-lg-png.h printer-sm-png.h
 ippeveps.o: ippeveps.c ippevecommon.h ../cups/cups.h ../cups/file.h \
   ../cups/base.h ../cups/ipp.h ../cups/http.h ../cups/array.h \
   ../cups/language.h ../cups/pwg.h ../cups/raster.h \
@@ -59,11 +107,9 @@ ippfind.o: ippfind.c ../cups/cups-private.h ../cups/string-private.h \
   \
   \
   \
-  \
-  \
-  \
-  ../cups/language-private.h ../cups/transcode.h ../cups/pwg-private.h \
-  ../cups/thread.h ../cups/dnssd.h
+  ../cups/language-private.h \
+  ../cups/transcode.h ../cups/pwg-private.h ../cups/thread.h \
+  ../cups/dnssd.h
 ipptool.o: ipptool.c ../cups/cups-private.h ../cups/string-private.h \
   ../config.h ../cups/base.h ../cups/debug-internal.h \
   ../cups/debug-private.h ../cups/ipp-private.h ../cups/cups.h \
@@ -87,9 +133,6 @@ ipptool.o: ipptool.c ../cups/cups-private.h ../cups/string-private.h \
   \
   \
   \
-  \
-  \
-  \
-  ../cups/language-private.h ../cups/transcode.h ../cups/pwg-private.h \
-  ../cups/thread.h ../cups/raster-testpage.h ../cups/raster-private.h \
-  ../cups/raster.h
+  ../cups/language-private.h \
+  ../cups/transcode.h ../cups/pwg-private.h ../cups/thread.h \
+  ../cups/raster-testpage.h ../cups/raster-private.h ../cups/raster.h
index 0afe31f514e0a91bc238d79b9e67f1f89f26703d..e1ea554236b0c014a2b10591b5a1bcabc7ab4914 100644 (file)
@@ -1,9 +1,7 @@
 #
 # IPP tools makefile for CUPS.
 #
-# Copyright © 2020-2024 by OpenPrinting.
-# Copyright © 2007-2019 by Apple Inc.
-# Copyright © 1997-2006 by Easy Software Products, all rights reserved.
+# Copyright © 2020-2025 by OpenPrinting.
 #
 # Licensed under Apache License v2.0.  See the file "LICENSE" for more
 # information.
@@ -13,12 +11,16 @@ include ../Makedefs
 
 
 OBJS           =       \
+                       cups-oauth.o \
+                       cups-x509.o \
                        ippevepcl.o \
                        ippeveprinter.o \
                        ippeveps.o \
                        ippfind.o \
                        ipptool.o
 IPPTOOLS       =       \
+                       cups-oauth \
+                       cups-x509 \
                        ippeveprinter \
                        $(IPPFIND_BIN) \
                        ipptool
@@ -142,6 +144,26 @@ uninstall:
 local: ippeveprinter-static ipptool-static
 
 
+#
+# cups-oauth
+#
+
+cups-oauth:    cups-oauth.o ../cups/$(LIBCUPS)
+       echo Linking $@...
+       $(LD_CC) $(ALL_LDFLAGS) -o $@ cups-oauth.o $(DNSSDLIBS) $(LINKCUPS)
+       $(CODE_SIGN) -s "$(CODE_SIGN_IDENTITY)" $@
+
+
+#
+# cups-x509
+#
+
+cups-x509:     cups-x509.o ../cups/$(LIBCUPS)
+       echo Linking $@...
+       $(LD_CC) $(ALL_LDFLAGS) -o $@ cups-x509.o $(DNSSDLIBS) $(LINKCUPS)
+       $(CODE_SIGN) -s "$(CODE_SIGN_IDENTITY)" $@
+
+
 #
 # ippeveprinter
 #
diff --git a/tools/cups-oauth.c b/tools/cups-oauth.c
new file mode 100644 (file)
index 0000000..c698d46
--- /dev/null
@@ -0,0 +1,545 @@
+//
+// OAuth utility for CUPS.
+//
+// Copyright © 2024-2025 by OpenPrinting.
+//
+// Licensed under Apache License v2.0.  See the file "LICENSE" for more
+// information.
+//
+// Usage: cups-oauth [OPTIONS] [COMMAND [ARGUMENT(S)]]
+//
+// Commands:
+//
+//   authorize [RESOURCE]
+//   clear [RESOURCE]
+//   get-access-token [RESOURCE]
+//   get-client-id
+//   get-metadata [NAME]
+//   get-refresh-token [RESOURCE]
+//   get-user-id [RESOURCE] [NAME]
+//   set-access-token [RESOURCE] TOKEN
+//   set-client-data CLIENT-ID CLIENT-SECRET
+//
+// Options:
+//
+//   --help
+//   --version
+//   -a OAUTH-URI
+//   -s SCOPE(S)
+//
+
+#include <cups/cups-private.h>
+#include <cups/oauth.h>
+
+
+//
+// Macro for localized text...
+//
+
+#  define _(x) x
+
+
+//
+// Local functions...
+//
+
+static int     do_authorize(const char *oauth_uri, const char *scopes, const char *resource_uri);
+static int     do_clear(const char *oauth_uri, const char *resource_uri);
+static int     do_get_access_token(const char *oauth_uri, const char *resource_uri);
+static int     do_get_client_id(const char *oauth_uri);
+static int     do_get_metadata(const char *oauth_uri, const char *name);
+static int     do_get_user_id(const char *oauth_uri, const char *resource_uri, const char *name);
+static int     do_set_access_token(const char *oauth_uri, const char *resource_uri, const char *token);
+static int     do_set_client_data(const char *oauth_uri, const char *client_id, const char *client_secret);
+static int     usage(FILE *out);
+
+
+//
+// 'main()' - Main entry.
+//
+
+int                                    // O - Exit status
+main(int  argc,                                // I - Number of command-line arguments
+     char *argv[])                     // I - Command-line arguments
+{
+  int          i;                      // Looping var
+  const char   *opt,                   // Current option
+               *oauth_uri = getenv("CUPS_OAUTH_URI"),
+                                       // OAuth authorization server URI
+               *scopes = getenv("CUPS_OAUTH_SCOPES");
+                                       // Scopes
+
+
+  // Parse the command-line...
+  for (i = 1; i < argc; i ++)
+  {
+    if (!strcmp(argv[i], "--help"))
+    {
+      return (usage(stdout));
+    }
+    else if (!strcmp(argv[i], "--version"))
+    {
+      puts(CUPS_SVERSION);
+      exit(0);
+    }
+    else if (!strncmp(argv[i], "--", 2))
+    {
+      _cupsLangPrintf(stderr, _("%s: Unknown option '%s'."), "cups-oauth", argv[i]);
+      return (usage(stderr));
+    }
+    else if (argv[i][0] == '-' && argv[i][1] != '-')
+    {
+      for (opt = argv[i] + 1; *opt; opt ++)
+      {
+        switch (*opt)
+        {
+          case 'a' : // -a AUTH-URI
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-oauth: Missing Authorization Server URI after '-a'."));
+                return (usage(stderr));
+              }
+
+              oauth_uri = argv[i];
+              break;
+
+          case 's' : // -s SCOPE(S)
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-oauth: Missing scope(s) after '-s'."));
+                return (usage(stderr));
+              }
+
+              scopes = argv[i];
+              break;
+
+          default :
+              _cupsLangPrintf(stderr, _("%s: Unknown option '-%c'."), "cups-oauth", *opt);
+              return (usage(stderr));
+        }
+      }
+    }
+    else if (!oauth_uri)
+    {
+      _cupsLangPuts(stderr, _("cups-oauth: No authorization server specified."));
+      return (usage(stderr));
+    }
+    else if (!strcmp(argv[i], "authorize"))
+    {
+      // authorize [RESOURCE]
+      i ++;
+      return (do_authorize(oauth_uri, scopes, argv[i]));
+    }
+    else if (!strcmp(argv[i], "clear"))
+    {
+      // clear [RESOURCE]
+      i ++;
+      return (do_clear(oauth_uri, argv[i]));
+    }
+    else if (!strcmp(argv[i], "get-access-token"))
+    {
+      // get-access-token [RESOURCE]
+      i ++;
+      return (do_get_access_token(oauth_uri, argv[i]));
+    }
+    else if (!strcmp(argv[i], "get-client-id"))
+    {
+      // get-client-id
+      i ++;
+      return (do_get_client_id(oauth_uri));
+    }
+    else if (!strcmp(argv[i], "get-metadata"))
+    {
+      // get-metadata [NAME]
+      i ++;
+      return (do_get_metadata(oauth_uri, argv[i]));
+    }
+    else if (!strcmp(argv[i], "get-user-id"))
+    {
+      // get-user-id [RESOURCE] [NAME]
+      i ++;
+      if (i < argc)
+      {
+        if (!strncmp(argv[i], "ipp://", 6) || !strncmp(argv[i], "ipps://", 7) || !strncmp(argv[i], "http://", 7) || !strncmp(argv[i], "https://", 8))
+         return (do_get_user_id(oauth_uri, argv[i], argv[i + 1]));
+        else
+         return (do_get_user_id(oauth_uri, /*resource_uri*/NULL, argv[i]));
+      }
+      else
+      {
+        return (do_get_user_id(oauth_uri, /*resource_uri*/NULL, /*name*/NULL));
+      }
+    }
+    else if (!strcmp(argv[i], "set-access-token"))
+    {
+      // set-access-token [RESOURCE] TOKEN
+      i ++;
+      if (i >= argc)
+      {
+       _cupsLangPuts(stderr, _("cups-oauth: Missing resource URI and/or access token."));
+       return (usage(stderr));
+      }
+
+      return (do_set_access_token(oauth_uri, argv[i], argv[i + 1]));
+    }
+    else if (!strcmp(argv[i], "set-client-data"))
+    {
+      // set-client-data CLIENT-ID CLIENT-DATA
+      i ++;
+      if ((i + 1) >= argc)
+      {
+       _cupsLangPuts(stderr, _("cups-oauth: Missing client_id and/or client_secret."));
+       return (usage(stderr));
+      }
+
+      return (do_set_client_data(oauth_uri, argv[i], argv[i + 1]));
+    }
+    else
+    {
+      _cupsLangPrintf(stderr, _("cups-oauth: Unknown command '%s'."), argv[i]);
+      return (usage(stderr));
+    }
+  }
+
+  // If we get this far, show usage...
+  return (usage(argc == 1 ? stdout : stderr));
+}
+
+
+//
+// 'do_authorize()' - Authorize access.
+//
+
+static int                             // O - Exit status
+do_authorize(const char *oauth_uri,    // I - Authorization Server URI
+             const char *scopes,       // I - Scope(s)
+             const char *resource_uri) // I - Resource URI
+{
+  int          status = 1;             // Exit status
+  cups_json_t  *metadata;              // Server metadata
+  char         *auth_code = NULL,      // Authorization code
+               *access_token = NULL;   // Access token
+  time_t       access_expires;         // Expiration date
+
+
+  // Get the server metadata...
+  if ((metadata = cupsOAuthGetMetadata(oauth_uri)) == NULL)
+  {
+    _cupsLangPrintf(stderr, _("cups-oauth: Unable to get metadata for '%s': %s"), oauth_uri, cupsGetErrorString());
+    return (1);
+  }
+
+  // Authorize...
+  if ((auth_code = cupsOAuthGetAuthorizationCode(oauth_uri, metadata, resource_uri, scopes, /*redirect_uri*/NULL)) == NULL)
+  {
+    _cupsLangPrintf(stderr, _("cups-oauth: Unable to get authorization from '%s': %s"), oauth_uri, cupsGetErrorString());
+    goto done;
+  }
+
+  // Get the access token...
+  if ((access_token = cupsOAuthGetTokens(oauth_uri, metadata, resource_uri, auth_code, CUPS_OGRANT_AUTHORIZATION_CODE, CUPS_OAUTH_REDIRECT_URI, &access_expires)) == NULL)
+  {
+    _cupsLangPrintf(stderr, _("cups-oauth: Unable to get access token from '%s': %s"), oauth_uri, cupsGetErrorString());
+    goto done;
+  }
+
+  // Show access token
+  puts(access_token);
+
+  status = 0;
+
+  // Clean up and return...
+  done:
+
+  cupsJSONDelete(metadata);
+  free(auth_code);
+  free(access_token);
+
+  return (status);
+}
+
+
+//
+// 'do_clear()' - Clear authorization information.
+//
+
+static int                             // O - Exit status
+do_clear(const char *oauth_uri,                // I - Authorization Server URI
+         const char *resource_uri)     // I - Resource URI
+{
+  cupsOAuthClearTokens(oauth_uri, resource_uri);
+
+  return (0);
+}
+
+
+//
+// 'do_get_access_token()' - Get an access token.
+//
+
+static int                             // O - Exit status
+do_get_access_token(
+    const char *oauth_uri,             // I - Authorization Server URI
+    const char *resource_uri)          // I - Resource URI
+{
+  char         *access_token;          // Access token
+  time_t       access_expires;         // Expiration date
+
+
+  if ((access_token = cupsOAuthCopyAccessToken(oauth_uri, resource_uri, &access_expires)) != NULL)
+  {
+    puts(access_token);
+    free(access_token);
+    return (0);
+  }
+
+  return (1);
+}
+
+
+//
+// 'do_get_client_id()' - Get the client ID value.
+//
+
+static int                             // O - Exit status
+do_get_client_id(
+    const char *oauth_uri)             // I - Authorization Server URI
+{
+  char *client_id;                     // Client ID
+
+
+  if ((client_id = cupsOAuthCopyClientId(oauth_uri, CUPS_OAUTH_REDIRECT_URI)) != NULL)
+  {
+    puts(client_id);
+    free(client_id);
+    return (0);
+  }
+
+  return (1);
+}
+
+
+//
+// 'do_get_metadata()' - Get authorization server metadata.
+//
+
+static int                             // O - Exit status
+do_get_metadata(const char *oauth_uri, // I - Authorization Server URI
+                const char *name)      // I - Field name
+{
+  cups_json_t  *metadata;              // Metadata
+  char         *json;                  // JSON string
+
+
+  // Get the metadata...
+  if ((metadata = cupsOAuthGetMetadata(oauth_uri)) == NULL)
+  {
+    _cupsLangPrintf(stderr, _("cups-oauth: Unable to get metadata for '%s': %s"), oauth_uri, cupsGetErrorString());
+    return (1);
+  }
+
+  // Show metadata...
+  if (name)
+  {
+    cups_json_t        *value = cupsJSONFind(metadata, name);
+                                       // Metadata value
+
+    if (value)
+    {
+      switch (cupsJSONGetType(value))
+      {
+        case CUPS_JTYPE_NULL :
+            puts("null");
+            break;
+
+        case CUPS_JTYPE_FALSE :
+            puts("false");
+            break;
+
+        case CUPS_JTYPE_TRUE :
+            puts("true");
+            break;
+
+        case CUPS_JTYPE_NUMBER :
+            printf("%g\n", cupsJSONGetNumber(value));
+            break;
+
+        case CUPS_JTYPE_STRING :
+            puts(cupsJSONGetString(value));
+            break;
+
+        default :
+            if ((json = cupsJSONExportString(value)) != NULL)
+            {
+              puts(json);
+              free(json);
+            }
+            break;
+      }
+
+      return (0);
+    }
+    else
+    {
+      return (1);
+    }
+  }
+  else if ((json = cupsJSONExportString(metadata)) != NULL)
+  {
+    puts(json);
+    free(json);
+  }
+
+  return (0);
+}
+
+
+//
+// 'do_get_user_id()' - Get user identification.
+//
+
+static int                             // O - Exit status
+do_get_user_id(
+    const char *oauth_uri,             // I - Authorization Server URI
+    const char *resource_uri,          // I - Resource URI
+    const char *name)                  // I - Claim name
+{
+  cups_jwt_t   *user_id;               // User ID information
+  cups_json_t  *claims;                // Claims
+  char         *json,                  // JSON string
+               date[256];              // Date
+
+
+  // Get the user_id...
+  if ((user_id = cupsOAuthCopyUserId(oauth_uri, resource_uri)) == NULL)
+  {
+    _cupsLangPrintf(stderr, _("cups-oauth: Unable to get user ID for '%s': %s"), oauth_uri, cupsGetErrorString());
+    return (1);
+  }
+
+  claims = cupsJWTGetClaims(user_id);
+
+  // Show user information...
+  if (name)
+  {
+    cups_json_t        *value = cupsJSONFind(claims, name);
+                                       // Claim value
+
+    if (value)
+    {
+      switch (cupsJSONGetType(value))
+      {
+        case CUPS_JTYPE_NULL :
+            puts("null");
+            break;
+
+        case CUPS_JTYPE_FALSE :
+            puts("false");
+            break;
+
+        case CUPS_JTYPE_TRUE :
+            puts("true");
+            break;
+
+        case CUPS_JTYPE_NUMBER :
+            if (!strcmp(name, "exp") || !strcmp(name, "iat") || !strcmp(name, "nbf"))
+              puts(httpGetDateString2((time_t)cupsJSONGetNumber(value), date, sizeof(date)));
+            else
+             printf("%g\n", cupsJSONGetNumber(value));
+            break;
+
+        case CUPS_JTYPE_STRING :
+            puts(cupsJSONGetString(value));
+            break;
+
+        default :
+            if ((json = cupsJSONExportString(value)) != NULL)
+            {
+              puts(json);
+              free(json);
+            }
+            break;
+      }
+
+      return (0);
+    }
+    else
+    {
+      return (1);
+    }
+  }
+  else if ((json = cupsJSONExportString(claims)) != NULL)
+  {
+    puts(json);
+    free(json);
+  }
+
+  return (0);
+}
+
+
+//
+// 'do_set_access_token()' - Set the access token.
+//
+
+static int                             // O - Exit status
+do_set_access_token(
+    const char *oauth_uri,             // I - Authorization Server URI
+    const char *resource_uri,          // I - Resource URI
+    const char *token)                 // I - Access token
+{
+  cupsOAuthSaveTokens(oauth_uri, resource_uri, token, /*access_expires*/time(NULL) + 365 * 86400, /*user_id*/NULL, /*refresh_token*/NULL);
+
+  return (0);
+}
+
+
+//
+// 'do_set_client_data()' - Save client_id and client_secret values.
+//
+
+static int                             // O - Exit status
+do_set_client_data(
+    const char *oauth_uri,             // I - Authorization Server URI
+    const char *client_id,             // I - Client ID
+    const char *client_secret)         // I - Client secret
+{
+  cupsOAuthSaveClientData(oauth_uri, CUPS_OAUTH_REDIRECT_URI, client_id, client_secret);
+
+  return (0);
+}
+
+
+//
+// 'usage()' - Show usage.
+//
+
+static int                             // O - Exit status
+usage(FILE *out)                       // I - Output file
+{
+  _cupsLangPuts(out, _("Usage: cups-oauth [OPTIONS] [COMMAND [ARGUMENT(S)]]"));
+  _cupsLangPuts(out, "");
+  _cupsLangPuts(out, _("Commands:"));
+  _cupsLangPuts(out, "");
+  _cupsLangPuts(out, _("authorize [RESOURCE]           Authorize access to a resource"));
+  _cupsLangPuts(out, _("clear [RESOURCE]               Clear the authorization for a resource"));
+  _cupsLangPuts(out, _("get-access-token [RESOURCE]    Get the current access token"));
+  _cupsLangPuts(out, _("get-client-id                  Get the client ID for the authorization server"));
+  _cupsLangPuts(out, _("get-metadata [NAME]            Get metadata from the authorization server"));
+  _cupsLangPuts(out, _("get-user-id [RESOURCE] [NAME]  Get the authorized user ID"));
+  _cupsLangPuts(out, _("set-access-token [RESOURCE] TOKEN\n"
+                      "                               Set the current access token"));
+  _cupsLangPuts(out, _("set-client-data CLIENT-ID CLIENT-SECRET\n"
+                      "                               Set the client ID and secret for the authorization server."));
+  _cupsLangPuts(out, "");
+  _cupsLangPuts(out, _("Options:"));
+  _cupsLangPuts(out, "");
+  _cupsLangPuts(out, _("--help                         Show this help"));
+  _cupsLangPuts(out, _("--version                      Show the program version"));
+  _cupsLangPuts(out, _("-a OAUTH-URI                   Specify the OAuth authorization server URL"));
+  _cupsLangPuts(out, _("-s SCOPE(S)                    Specify the scope(s) to authorize"));
+
+  return (out == stdout ? 0 : 1);
+}
diff --git a/tools/cups-x509.c b/tools/cups-x509.c
new file mode 100644 (file)
index 0000000..dcd94a3
--- /dev/null
@@ -0,0 +1,871 @@
+//
+// X.509 credentials utiltiy for CUPS.
+//
+// Copyright © 2022-2025 by OpenPrinting.
+//
+// Licensed under Apache License v2.0.  See the file "LICENSE" for more
+// information.
+//
+// Usage: cups-x509 [OPTIONS] [COMMAND] [ARGUMENT(S)]
+//
+// Commands:
+//
+//   ca COMMON-NAME             Sign a CSR to produce a certificate.
+//   cacert COMMON-NAME         Create a CA certificate.
+//   cert COMMON-NAME           Create a certificate.
+//   client URI                 Connect to URI.
+//   csr COMMON-NAME            Create a certificate signing request.
+//   server COMMON-NAME[:PORT]  Run a HTTPS server (default port 8NNN.)
+//   show COMMON-NAME           Show stored credentials for COMMON-NAME.
+//
+// Options:
+//
+//   --help                     Show program help
+//   --pin                      Pin the certificate found by the client command
+//   --require-ca               Require a CA-signed certificate for the client command
+//   --version                  Show program version
+//   -C COUNTRY                 Set country
+//   -L LOCALITY                Set locality name
+//   -O ORGANIZATION            Set organization name
+//   -R CSR-FILENAME            Specify certificate signing request filename
+//   -S STATE                   Set state
+//   -U ORGANIZATIONAL-UNIT     Set organizational unit name
+//   -a SUBJECT-ALT-NAME        Add a subjectAltName
+//   -d DAYS                    Set expiration date in days
+//   -p PURPOSE                 Comma-delimited certificate purpose (serverAuth,
+//                              clientAuth, codeSigning, emailProtection,
+//                              timeStamping, OCSPSigning)
+//   -r ROOT-NAME               Name of root certificate
+//   -t TYPE                    Certificate type (rsa-2048, rsa-3072, rsa-4096,
+//                              ecdsa-p256, ecdsa-p384, ecdsa-p521)
+//   -u USAGE                   Comma-delimited key usage (digitalSignature,
+//                              nonRepudiation, keyEncipherment,
+//                              dataEncipherment, keyAgreement, keyCertSign,
+//                              cRLSign, encipherOnly, decipherOnly, default-ca,
+//                              default-tls)
+//
+
+#include <cups/cups-private.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <poll.h>
+
+
+//
+// Macro for localized text...
+//
+
+#  define _(x) x
+
+
+//
+// Local functions...
+//
+
+static int     do_ca(const char *common_name, const char *csrfile, const char *root_name, int days);
+static int     do_cert(bool ca_cert, cups_credpurpose_t purpose, cups_credtype_t type, cups_credusage_t keyusage, const char *organization, const char *org_unit, const char *locality, const char *state, const char *country, const char *root_name, const char *common_name, size_t num_alt_names, const char **alt_names, int days);
+static int     do_client(const char *uri);
+static int     do_csr(cups_credpurpose_t purpose, cups_credtype_t type, cups_credusage_t keyusage, const char *organization, const char *org_unit, const char *locality, const char *state, const char *country, const char *common_name, size_t num_alt_names, const char **alt_names);
+static int     do_server(const char *host_port);
+static int     do_show(const char *common_name);
+static int     usage(FILE *fp);
+
+
+//
+// 'main()' - Main entry.
+//
+
+int                                    // O - Exit status
+main(int  argc,                                // I - Number of command-line arguments
+     char *argv[])                     // I - Command-line arguments
+{
+  int          i;                      // Looping var
+  const char   *command = NULL,        // Command
+               *arg = NULL,            // Argument for command
+               *opt,                   // Current option character
+               *csrfile = NULL,        // Certificste signing request filename
+               *root_name = NULL,      // Name of root certificate
+               *organization = NULL,   // Organization
+               *org_unit = NULL,       // Organizational unit
+               *locality = NULL,       // Locality
+               *state = NULL,          // State/province
+               *country = NULL,        // Country
+               *alt_names[100];        // Subject alternate names
+  size_t       num_alt_names = 0;
+  int          days = 365;             // Days until expiration
+  cups_credpurpose_t purpose = CUPS_CREDPURPOSE_SERVER_AUTH;
+                                       // Certificate purpose
+  cups_credtype_t type = CUPS_CREDTYPE_DEFAULT;
+                                       // Certificate type
+  cups_credusage_t keyusage = CUPS_CREDUSAGE_DEFAULT_TLS;
+                                       // Key usage
+
+
+  // Check command-line...
+  for (i = 1; i < argc; i ++)
+  {
+    if (!strcmp(argv[i], "--help"))
+    {
+      return (usage(stdout));
+    }
+    else if (!strcmp(argv[i], "--version"))
+    {
+      puts(CUPS_SVERSION);
+      exit(0);
+    }
+    else if (!strncmp(argv[i], "--", 2))
+    {
+      _cupsLangPrintf(stderr, _("%s: Unknown option '%s'."), "cups-x509", argv[i]);
+      return (usage(stderr));
+    }
+    else if (argv[i][0] == '-')
+    {
+      for (opt = argv[i] + 1; *opt; opt ++)
+      {
+        switch (*opt)
+        {
+          case 'C' : // -C COUNTRY
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-x509: Missing country after '-C'."));
+                return (usage(stderr));
+             }
+             country = argv[i];
+             break;
+
+          case 'L' : // -L LOCALITY
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-x509: Missing locality/city/town after '-L'."));
+                return (usage(stderr));
+             }
+             locality = argv[i];
+             break;
+
+          case 'O' : // -O ORGANIZATION
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-x509: Missing organization after '-O'."));
+                return (usage(stderr));
+             }
+             organization = argv[i];
+             break;
+
+          case 'R' : // -R CSR-FILENAME
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-x509: Missing CSR filename after '-R'."));
+                return (usage(stderr));
+             }
+             csrfile = argv[i];
+             break;
+
+          case 'S' : // -S STATE
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-x509: Missing state/province after '-S'."));
+                return (usage(stderr));
+             }
+             state = argv[i];
+             break;
+
+          case 'U' : // -U ORGANIZATIONAL-UNIT
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-x509: Missing organizational unit after '-U'."));
+                return (usage(stderr));
+             }
+             org_unit = argv[i];
+             break;
+
+          case 'a' : // -a SUBJECT-ALT-NAME
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-x509: Missing subjectAltName after '-a'."));
+                return (usage(stderr));
+             }
+             if (num_alt_names >= (sizeof(alt_names) / sizeof(alt_names[0])))
+             {
+               _cupsLangPuts(stderr, _("cups-x509: Too many subjectAltName values."));
+               return (1);
+             }
+             alt_names[num_alt_names ++] = argv[i];
+             break;
+
+          case 'd' : // -d DAYS
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-x509: Missing expiration days after '-d'."));
+                return (usage(stderr));
+             }
+             if ((days = atoi(argv[i])) <= 0)
+             {
+               _cupsLangPrintf(stderr, _("cups-x509: Bad DAYS value '%s' after '-d'."), argv[i]);
+               return (1);
+             }
+             break;
+
+          case 'p' : // -p PURPOSE
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-x509: Missing purpose after '-p'."));
+                return (usage(stderr));
+             }
+             purpose = 0;
+             if (strstr(argv[i], "serverAuth"))
+               purpose |= CUPS_CREDPURPOSE_SERVER_AUTH;
+             if (strstr(argv[i], "clientAuth"))
+               purpose |= CUPS_CREDPURPOSE_CLIENT_AUTH;
+             if (strstr(argv[i], "codeSigning"))
+               purpose |= CUPS_CREDPURPOSE_CODE_SIGNING;
+             if (strstr(argv[i], "emailProtection"))
+               purpose |= CUPS_CREDPURPOSE_EMAIL_PROTECTION;
+             if (strstr(argv[i], "timeStamping"))
+               purpose |= CUPS_CREDPURPOSE_TIME_STAMPING;
+             if (strstr(argv[i], "OCSPSigning"))
+               purpose |= CUPS_CREDPURPOSE_OCSP_SIGNING;
+              if (purpose == 0)
+              {
+                _cupsLangPrintf(stderr, _("cups-x509: Bad purpose '%s'."), argv[i]);
+                return (usage(stderr));
+             }
+             break;
+
+          case 'r' : // -r ROOT-NAME
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-x509: Missing root name after '-r'."));
+                return (usage(stderr));
+             }
+             root_name = argv[i];
+             break;
+
+          case 't' : // -t TYPE
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-x509: Missing certificate type after '-t'."));
+                return (usage(stderr));
+             }
+             if (!strcmp(argv[i], "default"))
+             {
+               type = CUPS_CREDTYPE_DEFAULT;
+             }
+             else if (!strcmp(argv[i], "rsa-2048"))
+             {
+               type = CUPS_CREDTYPE_RSA_2048_SHA256;
+             }
+             else if (!strcmp(argv[i], "rsa-3072"))
+             {
+               type = CUPS_CREDTYPE_RSA_3072_SHA256;
+             }
+             else if (!strcmp(argv[i], "rsa-4096"))
+             {
+               type = CUPS_CREDTYPE_RSA_4096_SHA256;
+             }
+             else if (!strcmp(argv[i], "ecdsa-p256"))
+             {
+               type = CUPS_CREDTYPE_ECDSA_P256_SHA256;
+             }
+             else if (!strcmp(argv[i], "ecdsa-p384"))
+             {
+               type = CUPS_CREDTYPE_ECDSA_P384_SHA256;
+             }
+             else if (!strcmp(argv[i], "ecdsa-p521"))
+             {
+               type = CUPS_CREDTYPE_ECDSA_P521_SHA256;
+             }
+             else
+             {
+               _cupsLangPrintf(stderr, _("cups-x509: Bad certificate type '%s'."), argv[i]);
+               return (usage(stderr));
+             }
+             break;
+
+          case 'u' : // -u USAGE
+              i ++;
+              if (i >= argc)
+              {
+                _cupsLangPuts(stderr, _("cups-x509: Missing key usage after '-u'."));
+                return (usage(stderr));
+             }
+             keyusage = 0;
+             if (strstr(argv[i], "default-ca"))
+               keyusage = CUPS_CREDUSAGE_DEFAULT_CA;
+             if (strstr(argv[i], "default-tls"))
+               keyusage = CUPS_CREDUSAGE_DEFAULT_TLS;
+             if (strstr(argv[i], "digitalSignature"))
+               keyusage |= CUPS_CREDUSAGE_DIGITAL_SIGNATURE;
+             if (strstr(argv[i], "nonRepudiation"))
+               keyusage |= CUPS_CREDUSAGE_NON_REPUDIATION;
+             if (strstr(argv[i], "keyEncipherment"))
+               keyusage |= CUPS_CREDUSAGE_KEY_ENCIPHERMENT;
+             if (strstr(argv[i], "dataEncipherment"))
+               keyusage |= CUPS_CREDUSAGE_DATA_ENCIPHERMENT;
+             if (strstr(argv[i], "keyAgreement"))
+               keyusage |= CUPS_CREDUSAGE_KEY_AGREEMENT;
+             if (strstr(argv[i], "keyCertSign"))
+               keyusage |= CUPS_CREDUSAGE_KEY_CERT_SIGN;
+             if (strstr(argv[i], "cRLSign"))
+               keyusage |= CUPS_CREDUSAGE_CRL_SIGN;
+             if (strstr(argv[i], "encipherOnly"))
+               keyusage |= CUPS_CREDUSAGE_ENCIPHER_ONLY;
+             if (strstr(argv[i], "decipherOnly"))
+               keyusage |= CUPS_CREDUSAGE_DECIPHER_ONLY;
+             if (keyusage == 0)
+             {
+               _cupsLangPrintf(stderr, _("cups-x509: Bad key usage '%s'."), argv[i]);
+               return (usage(stderr));
+             }
+             break;
+
+          default :
+              _cupsLangPrintf(stderr, _("%s: Unknown option '-%c'."), "cups-x509", *opt);
+              return (usage(stderr));
+       }
+      }
+    }
+    else if (!command)
+    {
+      command = argv[i];
+    }
+    else if (!arg)
+    {
+      arg = argv[i];
+    }
+    else
+    {
+      _cupsLangPrintf(stderr, _("%s: Unknown option '%s'."), "cups-x509", argv[i]);
+      return (usage(stderr));
+    }
+  }
+
+  if (!command || !arg)
+  {
+    _cupsLangPuts(stderr, _("cups-x509: Missing command argument."));
+    return (usage(stderr));
+  }
+
+  // Run the corresponding command...
+  if (!strcmp(command, "ca"))
+  {
+    return (do_ca(arg, csrfile, root_name, days));
+  }
+  else if (!strcmp(command, "cacert"))
+  {
+    return (do_cert(true, purpose, type, keyusage, organization, org_unit, locality, state, country, root_name, arg, num_alt_names, alt_names, days));
+  }
+  else if (!strcmp(command, "cert"))
+  {
+    return (do_cert(false, purpose, type, keyusage, organization, org_unit, locality, state, country, root_name, arg, num_alt_names, alt_names, days));
+  }
+  else if (!strcmp(command, "client"))
+  {
+    return (do_client(arg));
+  }
+  else if (!strcmp(command, "csr"))
+  {
+    return (do_csr(purpose, type, keyusage, organization, org_unit, locality, state, country, arg, num_alt_names, alt_names));
+  }
+  else if (!strcmp(command, "server"))
+  {
+    return (do_server(arg));
+  }
+  else if (!strcmp(command, "show"))
+  {
+    return (do_show(arg));
+  }
+  else
+  {
+    _cupsLangPrintf(stderr, _("cups-x509: Unknown command '%s'."), command);
+    return (usage(stderr));
+  }
+}
+
+
+//
+// 'do_ca()' - Test generating a certificate from a CSR.
+//
+
+static int                             // O - Exit status
+do_ca(const char *common_name,         // I - Common name
+      const char *csrfile,             // I - CSR filename, if any
+      const char *root_name,           // I - Root certificate name
+      int        days)                 // I - Number of days
+{
+  char *request,                       // Certificate request
+       *cert;                          // Certificate
+
+
+  if (csrfile)
+  {
+    int                csrfd = open(csrfile, O_RDONLY);
+                                       // File descriptor
+    struct stat        csrinfo;                // File information
+
+    if (csrfd < 0)
+    {
+      _cupsLangPrintf(stderr, _("cups-x509: Unable to access '%s': %s"), csrfile, strerror(errno));
+      return (1);
+    }
+
+    if (fstat(csrfd, &csrinfo))
+    {
+      _cupsLangPrintf(stderr, _("cups-x509: Unable to stat '%s': %s"), csrfile, strerror(errno));
+      close(csrfd);
+      return (1);
+    }
+
+    if ((request = malloc((size_t)csrinfo.st_size + 1)) == NULL)
+    {
+      _cupsLangPrintf(stderr, _("cups-x509: Unable to allocate memory for '%s': %s"), csrfile, strerror(errno));
+      close(csrfd);
+      return (1);
+    }
+
+    if (read(csrfd, request, (size_t)csrinfo.st_size) < (ssize_t)csrinfo.st_size)
+    {
+      _cupsLangPrintf(stderr, _("cups-x509: Unable to read '%s'."), csrfile);
+      close(csrfd);
+      return (1);
+    }
+
+    close(csrfd);
+    request[csrinfo.st_size] = '\0';
+  }
+  else if ((request = cupsCopyCredentialsRequest(/*path*/NULL, common_name)) == NULL)
+  {
+    _cupsLangPrintf(stderr, _("cups-x509: No request for '%s'."), common_name);
+    return (1);
+  }
+
+  if (!cupsSignCredentialsRequest(/*path*/NULL, common_name, request, root_name, CUPS_CREDPURPOSE_ALL, CUPS_CREDUSAGE_ALL, /*cb*/NULL, /*cb_data*/NULL, time(NULL) + days * 86400))
+  {
+    _cupsLangPrintf(stderr, _("cups-x509: Unable to create certificate (%s)"), cupsGetErrorString());
+    free(request);
+    return (1);
+  }
+
+  free(request);
+
+  if ((cert = cupsCopyCredentials(/*path*/NULL, common_name)) != NULL)
+  {
+    puts(cert);
+    free(cert);
+  }
+  else
+  {
+    _cupsLangPrintf(stderr, _("cups-x509: Unable to get generated certificate for '%s'."), common_name);
+    return (1);
+  }
+
+  return (0);
+}
+
+
+//
+// 'do_cert()' - Test creating a self-signed certificate.
+//
+
+static int                             // O - Exit status
+do_cert(
+    bool               ca_cert,                // I - `true` for a CA certificate, `false` for a regular one
+    cups_credpurpose_t purpose,                // I - Certificate purpose
+    cups_credtype_t    type,           // I - Certificate type
+    cups_credusage_t   keyusage,       // I - Key usage
+    const char         *organization,  // I - Organization
+    const char         *org_unit,      // I - Organizational unit
+    const char         *locality,      // I - Locality (city/town/etc.)
+    const char         *state,         // I - State/province
+    const char         *country,       // I - Country
+    const char         *root_name,     // I - Root certificate name
+    const char         *common_name,   // I - Common name
+    size_t             num_alt_names,  // I - Number of subjectAltName's
+    const char         **alt_names,    // I - subjectAltName's
+    int                days)           // I - Number of days until expiration
+{
+  char *cert,                          // Certificate
+       *key;                           // Private key
+
+
+  if (!cupsCreateCredentials(/*path*/NULL, ca_cert, purpose, type, keyusage, organization, org_unit, locality, state, country, common_name, /*email*/NULL, num_alt_names, alt_names, root_name, time(NULL) + days * 86400))
+  {
+    _cupsLangPrintf(stderr, _("cups-x509: Unable to create certificate (%s)"), cupsGetErrorString());
+    return (1);
+  }
+
+  if ((cert = cupsCopyCredentials(/*path*/NULL, common_name)) != NULL)
+  {
+    puts(cert);
+    free(cert);
+  }
+  else
+  {
+    _cupsLangPrintf(stderr, _("cups-x509: Unable to get generated certificate for '%s'."), common_name);
+    return (1);
+  }
+
+  if ((key = cupsCopyCredentialsKey(/*path*/NULL, common_name)) != NULL)
+  {
+    puts(key);
+    free(key);
+  }
+  else
+  {
+    _cupsLangPrintf(stderr, _("cups-x509: Unable to get generated private key for '%s'."), common_name);
+    return (1);
+  }
+
+  return (0);
+}
+
+
+//
+// 'do_client()' - Test connecting to a HTTPS server.
+//
+
+static int                             // O - Exit status
+do_client(const char *uri)             // I - URI
+{
+  http_t       *http;                  // HTTP connection
+  char         scheme[HTTP_MAX_URI],   // Scheme from URI
+               hostname[HTTP_MAX_URI], // Hostname from URI
+               username[HTTP_MAX_URI], // Username:password from URI
+               resource[HTTP_MAX_URI]; // Resource from URI
+  int          port;                   // Port number from URI
+  http_trust_t trust;                  // Trust evaluation for connection
+  char         *hcreds;                // Credentials from connection
+  char         hinfo[1024],            // String for connection credentials
+               datestr[256];           // Date string
+  static const char *trusts[] =                // Trust strings
+  { "OK", "Invalid", "Changed", "Expired", "Renewed", "Unknown" };
+
+
+  // Connect to the host and validate credentials...
+  if (httpSeparateURI(HTTP_URI_CODING_MOST, uri, scheme, sizeof(scheme), username, sizeof(username), hostname, sizeof(hostname), &port, resource, sizeof(resource)) < HTTP_URI_STATUS_OK)
+  {
+    _cupsLangPrintf(stderr, _("cups-x509: Bad URI '%s'."), uri);
+    return (1);
+  }
+
+  if ((http = httpConnect2(hostname, port, NULL, AF_UNSPEC, HTTP_ENCRYPTION_ALWAYS, 1, 30000, NULL)) == NULL)
+  {
+    _cupsLangPrintf(stderr, _("cups-x509: Unable to connect to '%s' on port %d: %s"), hostname, port, cupsGetErrorString());
+    return (1);
+  }
+
+  puts("TLS Server Credentials:");
+  if ((hcreds = httpCopyPeerCredentials(http)) != NULL)
+  {
+    trust = cupsGetCredentialsTrust(/*path*/NULL, hostname, hcreds, /*require_ca*/false);
+
+    cupsGetCredentialsInfo(hcreds, hinfo, sizeof(hinfo));
+
+//    printf("    Certificate Count: %u\n", (unsigned)cupsArrayGetCount(hcreds));
+    if (trust == HTTP_TRUST_OK)
+      puts("    Trust: OK");
+    else
+      printf("    Trust: %s (%s)\n", trusts[trust], cupsGetErrorString());
+    printf("    Expiration: %s\n", httpGetDateString2(cupsGetCredentialsExpiration(hcreds), datestr, sizeof(datestr)));
+    printf("     ValidName: %s\n", cupsAreCredentialsValidForName(hostname, hcreds) ? "true" : "false");
+    printf("          Info: \"%s\"\n", hinfo);
+
+    free(hcreds);
+  }
+  else
+  {
+    puts("    Not present (error).");
+  }
+
+  puts("");
+
+  return (do_show(hostname));
+}
+
+
+//
+// 'do_csr()' - Test creating a certificate signing request.
+//
+
+static int                             // O - Exit status
+do_csr(
+    cups_credpurpose_t purpose,                // I - Certificate purpose
+    cups_credtype_t    type,           // I - Certificate type
+    cups_credusage_t   keyusage,       // I - Key usage
+    const char         *organization,  // I - Organization
+    const char         *org_unit,      // I - Organizational unit
+    const char         *locality,      // I - Locality (city/town/etc.)
+    const char         *state,         // I - State/province
+    const char         *country,       // I - Country
+    const char         *common_name,   // I - Common name
+    size_t             num_alt_names,  // I - Number of subjectAltName's
+    const char         **alt_names)    // I - subjectAltName's
+{
+  char *csr;                           // Certificate request
+
+
+  if (!cupsCreateCredentialsRequest(/*path*/NULL, purpose, type, keyusage, organization, org_unit, locality, state, country, common_name, /*email*/NULL, num_alt_names, alt_names))
+  {
+    _cupsLangPrintf(stderr, _("cups-x509: Unable to create certificate request (%s)"), cupsGetErrorString());
+    return (1);
+  }
+
+  if ((csr = cupsCopyCredentialsRequest(/*path*/NULL, common_name)) != NULL)
+  {
+    puts(csr);
+    free(csr);
+  }
+  else
+  {
+    _cupsLangPrintf(stderr, _("cups-x509: Unable to get generated certificate request for '%s'."), common_name);
+    return (1);
+  }
+
+  return (0);
+}
+
+
+//
+// 'do_server()' - Test running a server.
+//
+
+static int                             // O - Exit status
+do_server(const char *host_port)       // I - Hostname/port
+{
+  char         host[256],              // Hostname
+               *hostptr;               // Pointer into hostname
+  int          port;                   // Port number
+  nfds_t       i,                      // Looping var
+               num_listeners = 0;      // Number of listeners
+  struct pollfd        listeners[2];           // Listeners
+  http_addr_t  addr;                   // Listen address
+  http_t       *http;                  // Client
+
+
+  // Get the host and port...
+  cupsCopyString(host, host_port, sizeof(host));
+  if ((hostptr = strrchr(host, ':')) != NULL)
+  {
+    // Extract the port number from the argument...
+    *hostptr++ = '\0';
+    port       = atoi(hostptr);
+  }
+  else
+  {
+    // Use the default port 8NNN where NNN is the bottom 3 digits of the UID...
+    port = 8000 + (int)getuid() % 1000;
+  }
+
+  // Setup listeners for IPv4 and IPv6...
+  memset(&addr, 0, sizeof(addr));
+  addr.ipv4.sin_family = AF_INET;
+
+  if ((listeners[num_listeners].fd = httpAddrListen(&addr, port)) > 0)
+  {
+    listeners[num_listeners].events = POLLIN | POLLERR;
+    num_listeners ++;
+  }
+
+  addr.ipv6.sin6_family = AF_INET6;
+
+  if ((listeners[num_listeners].fd = httpAddrListen(&addr, port)) > 0)
+  {
+    listeners[num_listeners].events = POLLIN | POLLERR;
+    num_listeners ++;
+  }
+
+  if (num_listeners == 0)
+  {
+    _cupsLangPrintf(stderr, _("cups-x509: Unable to listen on port %d: %s"), port, cupsGetErrorString());
+    return (1);
+  }
+
+  printf("Listening for connections on port %d...\n", port);
+
+  // Set certificate info...
+  cupsSetServerCredentials(/*path*/NULL, host, true);
+
+  // Wait for connections...
+  for (;;)
+  {
+    http_state_t       state;          // HTTP request state
+    char               resource[1024]; // Resource path
+
+    // Look for new connections...
+    if (poll(listeners, num_listeners, 1000) < 0)
+    {
+      if (errno == EINTR || errno == EAGAIN)
+        continue;
+
+      perror("cups-x509: Unable to poll");
+      break;
+    }
+
+    // Try accepting a connection...
+    for (i = 0, http = NULL; i < num_listeners; i ++)
+    {
+      if (listeners[i].revents & POLLIN)
+      {
+        if ((http = httpAcceptConnection(listeners[i].fd, true)) != NULL)
+          break;
+
+        _cupsLangPrintf(stderr, _("cups-x509: Unable to accept connection: %s"), cupsGetErrorString());
+      }
+    }
+
+    if (!http)
+      continue;
+
+    // Negotiate a secure connection...
+    if (!httpSetEncryption(http, HTTP_ENCRYPTION_ALWAYS))
+    {
+      _cupsLangPrintf(stderr, _("cups-x509: Unable to encrypt connection: %s"), cupsGetErrorString());
+      httpClose(http);
+      continue;
+    }
+
+    // Process a single request and then close it out...
+    while ((state = httpReadRequest(http, resource, sizeof(resource))) == HTTP_STATE_WAITING)
+      usleep(1000);
+
+    if (state == HTTP_STATE_ERROR)
+    {
+      if (httpGetError(http) == EPIPE)
+       _cupsLangPuts(stderr, _("cups-x509: Client closed connection."));
+      else
+       _cupsLangPrintf(stderr, _("cups-x509: Bad request line (%s)."), strerror(httpGetError(http)));
+    }
+    else if (state == HTTP_STATE_UNKNOWN_METHOD)
+    {
+      _cupsLangPuts(stderr, _("cups-x509: Bad/unknown operation."));
+    }
+    else if (state == HTTP_STATE_UNKNOWN_VERSION)
+    {
+      _cupsLangPuts(stderr, _("cups-x509: Bad HTTP version."));
+    }
+    else
+    {
+      printf("%s %s\n", httpStateString(state), resource);
+
+      if (state == HTTP_STATE_GET || state == HTTP_STATE_HEAD)
+      {
+        httpClearFields(http);
+        httpSetField(http, HTTP_FIELD_CONTENT_TYPE, "text/plain");
+        httpSetField(http, HTTP_FIELD_CONNECTION, "close");
+       httpSetLength(http, strlen(resource) + 1);
+       httpWriteResponse(http, HTTP_STATUS_OK);
+
+        if (state == HTTP_STATE_GET)
+        {
+          // Echo back the resource path...
+          httpWrite(http, resource, strlen(resource));
+          httpWrite(http, "\n", 1);
+          httpFlushWrite(http);
+        }
+      }
+      else
+      {
+        httpWriteResponse(http, HTTP_STATUS_BAD_REQUEST);
+      }
+    }
+
+    httpClose(http);
+  }
+
+  // Close listeners and return...
+  for (i = 0; i < num_listeners; i ++)
+    httpAddrClose(&addr, listeners[i].fd);
+
+  return (0);
+}
+
+
+//
+// 'do_show()' - Test showing stored certificates.
+//
+
+static int                             // O - Exit status
+do_show(const char *common_name)       // I - Common name
+{
+  char *tcreds,                        // Credentials from trust store
+       tinfo[1024],                    // String for trust store credentials
+       datestr[256];                   // Date string
+
+
+  printf("Trust Store for \"%s\":\n", common_name);
+
+  if ((tcreds = cupsCopyCredentials(/*path*/NULL, common_name)) != NULL)
+  {
+    cupsGetCredentialsInfo(tcreds, tinfo, sizeof(tinfo));
+
+//    printf("    Certificate Count: %u\n", (unsigned)cupsArrayGetCount(tcreds));
+    printf("    Expiration: %s\n", httpGetDateString2(cupsGetCredentialsExpiration(tcreds), datestr, sizeof(datestr)));
+    printf("     ValidName: %s\n", cupsAreCredentialsValidForName(common_name, tcreds) ? "true" : "false");
+    printf("          Info: \"%s\"\n", tinfo);
+
+    free(tcreds);
+  }
+  else
+  {
+    puts("    Not present.");
+  }
+
+  return (0);
+}
+
+
+//
+// 'usage()' - Show program usage...
+//
+
+static int                             // O - Exit code
+usage(FILE *out)                       // I - Output file (stdout or stderr)
+{
+  _cupsLangPuts(out, _("Usage: cups-x509 [OPTIONS] [COMMAND] [ARGUMENT]"));
+  _cupsLangPuts(out, "");
+  _cupsLangPuts(out, _("Commands:"));
+  _cupsLangPuts(out, "");
+  _cupsLangPuts(out, _("ca COMMON-NAME             Sign a CSR to produce a certificate."));
+  _cupsLangPuts(out, _("cacert COMMON-NAME         Create a CA certificate."));
+  _cupsLangPuts(out, _("cert COMMON-NAME           Create a certificate."));
+  _cupsLangPuts(out, _("client URI                 Connect to URI."));
+  _cupsLangPuts(out, _("csr COMMON-NAME            Create a certificate signing request."));
+  _cupsLangPuts(out, _("server COMMON-NAME[:PORT]  Run a HTTPS server (default port 8NNN.)"));
+  _cupsLangPuts(out, _("show COMMON-NAME           Show stored credentials for COMMON-NAME."));
+  _cupsLangPuts(out, "");
+  _cupsLangPuts(out, _("Options:"));
+  _cupsLangPuts(out, _(""));
+  _cupsLangPuts(out, _("--help                         Show this help"));
+  _cupsLangPuts(out, _("--pin                          Pin certificate found by client command"));
+  _cupsLangPuts(out, _("--require-ca                   Require CA-signed certificate for client command"));
+  _cupsLangPuts(out, _("--version                      Show the program version"));
+  _cupsLangPuts(out, _("-C COUNTRY                     Set country."));
+  _cupsLangPuts(out, _("-L LOCALITY                    Set locality name."));
+  _cupsLangPuts(out, _("-O ORGANIZATION                Set organization name."));
+  _cupsLangPuts(out, _("-R CSR-FILENAME                Specify certificate signing request file."));
+  _cupsLangPuts(out, _("-S STATE                       Set state."));
+  _cupsLangPuts(out, _("-U ORGANIZATIONAL-UNIT         Set organizational unit name."));
+  _cupsLangPuts(out, _("-a SUBJECT-ALT-NAME            Add a subjectAltName."));
+  _cupsLangPuts(out, _("-d DAYS                        Set expiration date in days."));
+  _cupsLangPuts(out, _("-p PURPOSE                     Comma-delimited certificate purpose\n"
+                      "                               (serverAuth, clientAuth, codeSigning, emailProtection, timeStamping, OCSPSigning)"));
+  _cupsLangPuts(out, _("-r ROOT-NAME                   Name of root certificate"));
+  _cupsLangPuts(out, _("-t TYPE                        Certificate type\n"
+                      "                               (rsa-2048, rsa-3072, rsa-4096, ecdsa-p256, ecdsa-p384, ecdsa-p521)"));
+  _cupsLangPuts(out, _("-u USAGE                       Comma-delimited key usage\n"
+                      "                               (digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement, keyCertSign, cRLSign, encipherOnly, decipherOnly, default-ca, default-tls)"));
+
+  return (out == stderr);
+}