From: Michael R Sweet Date: Tue, 4 Mar 2025 18:43:37 +0000 (-0500) Subject: Add OAuth and X.509 utilities (Issue #1184) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=acf4ad4120682de12af1e094e67ff5bc40e30204;p=thirdparty%2Fcups.git Add OAuth and X.509 utilities (Issue #1184) --- diff --git a/.gitignore b/.gitignore index 1780afd5b7..81bc22ca2e 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ /conf/mime.convs /conf/pam.std /conf/snmp.conf +/cups/.testssl /cups/cachebench /cups/fuzzipp /cups/libcups.dylib @@ -149,6 +150,8 @@ /templates/*/header.tmpl /test/cups-str-*.html /test/*_log-* +/tools/cups-oauth +/tools/cups-x509 /tools/ippevepcl /tools/ippeveprinter /tools/ippeveprinter-static diff --git a/CHANGES.md b/CHANGES.md index afe7a30b61..8fe45e6983 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/cups/oauth.c b/cups/oauth.c index 5ed0cf3575..a1e49957df 100644 --- a/cups/oauth.c +++ b/cups/oauth.c @@ -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. // diff --git a/doc/Makefile b/doc/Makefile index fab908857f..38f2b4ce3b 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -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 index 0000000000..f95acb591a --- /dev/null +++ b/doc/help/man-cups-oauth.html @@ -0,0 +1,162 @@ + + + + + cups-oauth(1) + + +

cups-oauth(1)

+

Name

+

cups-oauth - interact with an oauth/openid authorization server +

+

Synopsis

+

cups-oauth +--help +
+cups-oauth +--version +
+cups-oauth +[ +-a +OAUTH-URI +] [ +-s +SCOPE(S) +] +authorize +[RESOURCE] +
+cups-oauth +[ +-a +OAUTH-URI +] +clear +[RESOURCE] +
+cups-oauth +[ +-a +OAUTH-URI +] +get-access-token +[RESOURCE] +
+cups-oauth +[ +-a +OAUTH-URI +] +get-client-id +
+cups-oauth +[ +-a +OAUTH-URI +] +get-metadata +[NAME] +
+cups-oauth +[ +-a +OAUTH-URI +] +get-user-id +[RESOURCE] +[NAME] +
+cups-oauth +[ +-a +OAUTH-URI +] +set-access-token +[RESOURCE] +TOKEN +
+cups-oauth +[ +-a +OAUTH-URI +] +set-client-data +CLIENT-ID +CLIENT-SECRET +

+

Description

+

The +cups-oauth +utility interacts with an OAuth/OpenID authorization server. +Authorizations are often linked to a resource (a printer URI, web page URL, etc.) +

+

Options

+

The following options are recognized by +cups-oauth: +

+

--help
+Show program usage. +

+

--version
+Show the CUPS version. +

+

-a OAUTH-URI
+Specifies the OAuth/OpenID authorization server URL. +

+

-s SCOPE(S)
+Specifies a space-delimited list of scope names to use when authorizing access. +The default is to request authorization for all supported scopes. +

+

Commands

+

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. +

+

Clear

+

Clears any authorization for the specified resource or for all resources if no resource URI is supplied. +

+

Get-Access-Token

+

Output the current, unexpired access token, if any, to the standard output. +

+

Get-Client-Id

+

Output the client ID string, if any, to the standard output. +

+

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. +

+

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. +

+

Set-Access-Token

+

Set the access token (which is sometimes also called an API key) for the specified resource or for all resources. +

+

Set-Client-Data

+

Set the client ID string and secret for an OAuth/OpenID authorization server. +

+

Environment Variables

+

The +CUPS_OAUTH_URI +environment variable sets the default OAuth/OpenID authorization server URL. +

+

The +CUPS_OAUTH_SCOPES +environment variable sets the default OAuth/OpenID scopes as a space-delimited list. +

+

Notes

+

CUPS uses a redirect URI of "http://127.0.0.1/" for all authorization on the local system. +

+

Examples

+

TBD +

+

See Also

+

cups(1) + +

+

Copyright

+

Copyright © 2025 by OpenPrinting. + + diff --git a/doc/help/man-cups-x509.html b/doc/help/man-cups-x509.html new file mode 100644 index 0000000000..8d0017cc22 --- /dev/null +++ b/doc/help/man-cups-x509.html @@ -0,0 +1,160 @@ + + + + + cups-x509(1) + + +

cups-x509(1)

+

Name

+

cups-x509 - description +

+

Synopsis

+

cups-x509 +--help +
+cups-x509 +--version +
+cups-x509 +[ +--pin +] [ +--require-ca +] [ +-C +COUNTRY +] [ +-L +LOCALITY +] [ +-O +ORGANIZATION +] [ +-R +CSR-FILENAME +] [ +-S +STATE-PROVINCE +] [ +-U +ORGANIZATIONAL-UNIT +] [ +-a +SUBJECT-ALT-NAME +] [ +-d +DAYS +] [ +-p +PURPOSE +] [ +-r +ROOT-NAME +] [ +-t +TYPE +] [ +-u +USAGE +] +COMMAND +[ARGUMENT(S)] +

+

Description

+

The +cups-x509 +utility manages X.509 certificates and certificate requests, and supports client and server tests. +

+

Options

+

The following options are recognized by +cups-x509: +

+

--help
+Show program usage. +

+

--pin
+Pin the server's X.509 certificate found by the client command. +

+

--require-ca
+Require the server's X.509 certificate found by the client command to be signed by a known CA. +

+

--version
+Show the CUPS version. +

+

-C COUNTRY
+Specify the country for new X.509 certificates and certificate requests. +

+

-L LOCALITY
+Specify the locality (city, town, etc.) for new X.509 certificates and certificate requests. +

+

-O ORGANIZATION
+Specify the organization name for new X.509 certificates and certificate requests. +

+

-R CSR-FILENAME
+Specify an X.509 certificate signing request in PEM format to be used when signing a certificate with the +ca +command. +

+

-S STATE-PROVINCE
+Specify the state/province name for new X.509 certificates and certificate requests. +

+

-U ORGANIZATIONAL-UNIT
+Specify the organizational unit name for new X.509 certificates and certificate requests. +

+

-a SUBJECT-ALT-NAME
+Specify an alternate name for new X.509 certificates and certificate requests. +

+

-d DAYS
+Specify the number of days before a new X.509 certificate will expire. +

+

-p PURPOSE
+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. +

+

-r ROOT-NAME
+Specify the common name of the X.509 root certificate to use. +The default root certificate is named "_site_". +

+

-t TYPE
+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. +

+

-u USAGE
+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. +

+

Commands

+

Ca Common-Name

+

Sign a certificate request for the specified common name. +

+

Cacert Common-Name

+

Create a CA certificate for the specified common name. +

+

Cert Common-Name

+

Create a certificate for the specified common name. +

+

Client Uri

+

Connect to the specified URI and validate the server's certificate. +

+

Csr Common-Name

+

Create a certificate signing request for the specified common name. +

+

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. +

+

Show Common-Name

+

Shows any stored credentials for the specified common name. +

+

Examples

+

TBD +

+

See Also

+

cups(1) + +

+

Copyright

+

Copyright © 2025 by OpenPrinting. + + diff --git a/man/Makefile b/man/Makefile index 83a0f6d5df..0dc8b07ad3 100644 --- a/man/Makefile +++ b/man/Makefile @@ -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 index 0000000000..7a7c0941aa --- /dev/null +++ b/man/cups-oauth.1 @@ -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 index 0000000000..2f42ab71b3 --- /dev/null +++ b/man/cups-x509.1 @@ -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. diff --git a/tools/Dependencies b/tools/Dependencies index c9194b6dc3..94ed960b22 100644 --- a/tools/Dependencies +++ b/tools/Dependencies @@ -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 diff --git a/tools/Makefile b/tools/Makefile index 0afe31f514..e1ea554236 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -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 index 0000000000..c698d46561 --- /dev/null +++ b/tools/cups-oauth.c @@ -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 +#include + + +// +// 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 index 0000000000..dcd94a310f --- /dev/null +++ b/tools/cups-x509.c @@ -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 +#include +#include +#include +#include +#include + + +// +// 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); +}