[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: shred\nko_fi: shredzone\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  pull_request:\n    branches:\n      - master\n  push:\n    branches:\n      - master\n\njobs:\n  build:\n    runs-on: ubuntu-22.04\n    strategy:\n      matrix:\n        java: [ '17', '21' ]\n    steps:\n    - uses: actions/checkout@v2\n    - name: Set up JDK\n      uses: actions/setup-java@v2\n      with:\n        distribution: 'temurin'\n        java-version: ${{ matrix.java }}\n    - name: Build\n      run: mvn --no-transfer-progress -B -P ci verify\n"
  },
  {
    "path": ".gitignore",
    "content": "target\n"
  },
  {
    "path": ".gitlab-ci.yml",
    "content": "stages:\n  - build\n  - test\n\nvariables:\n  DOCKER_TLS_CERTDIR: \"/certs\"\n  DOCKER_HOST: tcp://docker:2376\n  DOCKER_TLS_VERIFY: \"1\"\n  DOCKER_CERT_PATH: \"$DOCKER_TLS_CERTDIR/client\"\n\nimage: maven:3-eclipse-temurin-17\n\ncache:\n  key: ${CI_COMMIT_REF_SLUG}\n  paths:\n    - /root/.m2/repository/\n    - acme4j-client/target/\n    - acme4j-example/target/\n    - acme4j-it/target/\n    - acme4j-smime/target/\n    - target/\n\nbuild:\n  stage: build\n  script:\n    - apt-get update -y && apt-get install -y mkdocs\n    - mvn -B clean install mkdocs:build\n\ntest:\n  stage: test\n  services:\n    - name: docker:dind\n      alias: pebble, bammbamm\n  script:\n    - (cd acme4j-it; mvn -B docker:remove)\n    - mvn -B -P ci verify -DpebbleHost=pebble -DbammbammUrl=http://bammbamm:8055 -Ddocker.showLogs=true\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to _acme4j_\n\nThank you for taking your time to contribute!\n\n## Acceptance Criteria\n\nThese criteria must be met for a successful pull request:\n\n* Follow the [Style Guide](#style-guide).\n* If you add code, remember to add [unit tests](#unit-tests) that test your code.\n* All unit tests must run successfully.\n* Integration tests should run successfully, unless there is a good reason (e.g. waiting for a pending change in Pebble).\n* Your commits follow the [git commit](#git-commits) guide.\n* You accept that your code is distributed under the terms of [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).\n* You confirm that you did not use AI based code generators like GitHub Copilot for your contribution.\n\n## Style Guide\n\nOur style guide bases on [Oracle's Code conventions for the Java Programming Language](http://www.oracle.com/technetwork/java/codeconventions-150003.pdf). These additional rules apply:\n\n* Indentation is 4 spaces. Do not use tabs!\n* Remove trailing spaces.\n* Line length is 90 characters. You may exceed this length by a few characters if it is easier to read than a wrapped line.\n* `if`, `for` and `while` statements always use blocks, even for a single statement.\n* All types and methods must have a descriptive JavaDoc, except of `@Override` annotated methods. For plain getter and setter methods, `@param` and `@return` can be omitted.\n\n## Unit Tests\n\nMore than 80% of the code is covered by unit tests, and we would like to keep it that way.\n\n* Main functionalities must be covered by unit tests.\n* Corner cases should be covered by unit tests.\n* Common exception handling does not need to be tested.\n* No tests are required for code that is not expected to be executed (e.g. `UnsupportedEncodingException` when handling utf-8, or the empty private default constructor of a utility class).\n* Unit tests should not depend on external resources, as they might be temporarily unavailable at runtime.\n\nThere are no unit tests required for the `acme4j-example` and `acme4j-it` modules.\n\n## git Commits\n\nGood programming does not end with a clean source code, but should have pretty commits as well.\n\n* Always put separate concerns into separate commits.\n* If you have interim commits in your history, squash them with an interactive rebase before sending the pull request.\n* Use present tense and imperative mood in commit messages (\"fix bug #1234\", not \"fixed bug #1234\").\n* Always give meaningful commit messages (not just \"bug fix\").\n* The commit message must be concise and should not exceed 50 characters. Further explanations may follow in subsequent lines, with an empty line as separator.\n* Commits must compile and must not break unit tests.\n* Do not commit IDE generated files and directories (like `.project` or `.idea`).\n"
  },
  {
    "path": "LICENSE-APL.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# ACME Java Client ![build status](https://shredzone.org/badge/acme4j.svg) ![maven central](https://shredzone.org/maven-central/org.shredzone.acme4j/acme4j/badge.svg)\n\nThis is a Java client for the _Automatic Certificate Management Environment_ (ACME) protocol as specified in [RFC 8555](https://tools.ietf.org/html/rfc8555).\n\nACME is a protocol that a certificate authority (CA) and an applicant can use to automate the process of verification and certificate issuance.\n\nThis Java client helps to connect to an ACME server, and performing all necessary steps to manage certificates.\n\n## Features\n\n* Mature and stable code base. First release was in December 2015!\n* Fully [RFC 8555](https://tools.ietf.org/html/rfc8555) compliant\n* Supports the `http-01`, `dns-01`, and `tls-alpn-01` ([RFC 8737](https://tools.ietf.org/html/rfc8737)) challenges\n* Supports [RFC 8738](https://tools.ietf.org/html/rfc8738) IP identifier validation\n* Supports [RFC 8739](https://tools.ietf.org/html/rfc8739) short-term automatic certificate renewal (experimental)\n* Supports [RFC 8823](https://tools.ietf.org/html/rfc8823) for S/MIME certificates (experimental)\n* Supports [RFC 9444](https://tools.ietf.org/html/rfc9444) for subdomain validation\n* Supports [RFC 9773](https://tools.ietf.org/html/rfc9773) for renewal information\n* Supports [draft-ietf-acme-profiles-01](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/) for certificate profiles (experimental)\n* Supports [draft-ietf-acme-dns-account-label-02](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-account-label/) for DNS labeled with ACME account ID challenges (experimental)\n* Supports [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/) for dns-persist-01 challenges (experimental)\n* Easy to use Java API\n* Requires JRE 17 or higher\n* Supports [Actalis](https://www.actalis.com/), [Google Trust Services](https://pki.goog/), [Let's Encrypt](https://letsencrypt.org/), [SSL.com](https://www.ssl.com/), [ZeroSSL](https://zerossl.com/), and **all other CAs that comply with the ACME protocol (RFC 8555)**. Note that _acme4j_ is an independent project that is not supported or endorsed by any of the CAs.\n* Built with maven, packages available at [Maven Central](http://search.maven.org/#search|ga|1|g%3A%22org.shredzone.acme4j%22)\n* Extensive unit and integration tests\n* Adheres to [Semantic Versioning](https://semver.org/)\n\n## Dependencies\n\n* [Bouncy Castle](https://www.bouncycastle.org/)\n* [jose4j](https://bitbucket.org/b_c/jose4j/wiki/Home)\n* [slf4j](http://www.slf4j.org/)\n* For `acme4j-smime`: [Jakarta Mail](https://eclipse-ee4j.github.io/mail/), [Bouncy Castle](https://www.bouncycastle.org/)\n\n## Usage\n\n* See the [online documentation](https://shredzone.org/maven/acme4j/) about how to use _acme4j_.\n* For a quick start, take a look at [the source code of an example](https://shredzone.org/maven/acme4j/example.html).\n\n## Announcements\n\nFollow our Mastodon feed for release notes and other acme4j related news.\n\n* Mastodon: `@acme4j@foojay.social`\n* RSS: https://foojay.social/@acme4j.rss\n\n## Contribute\n\n* Fork the [Source code at Codeberg](https://codeberg.org/shred/acme4j) or [GitHub](https://github.com/shred/acme4j). Feel free to send pull requests (see [Contributing](CONTRIBUTING.md) for the rules).\n* Found a bug? [File a bug report!](https://codeberg.org/shred/acme4j/issues) ([GitHub](https://github.com/shred/acme4j/issues))\n\n## License\n\n_acme4j_ is open source software. The source code is distributed under the terms of [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).\n\n## Acknowledgements\n\n* I would like to thank Brian Campbell and all the other [jose4j](https://bitbucket.org/b_c/jose4j/wiki/Home) developers. _acme4j_ would not exist without your excellent work.\n* Thanks to [Daniel McCarney](https://github.com/cpu) for his help with the ACME protocol, Pebble, and Boulder.\n* [Ulrich Krause](https://github.com/eknori) for his help to make _acme4j_ run on IBM Java VMs.\n* I also like to thank [everyone who contributed to _acme4j_](https://codeberg.org/shred/acme4j/activity/contributors).\n* The Mastodon account is hosted by [foojay.io](https://foojay.io).\n"
  },
  {
    "path": "acme4j-client/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n *\n * acme4j - ACME Java client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n *\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.shredzone.acme4j</groupId>\n        <artifactId>acme4j</artifactId>\n        <version>5.1.1-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>acme4j-client</artifactId>\n\n    <name>acme4j Client</name>\n    <description>ACME client for Java</description>\n\n    <build>\n        <resources>\n            <resource>\n                <directory>src/main/resources</directory>\n            </resource>\n            <resource>\n                <directory>src/main/resources-filtered</directory>\n                <filtering>true</filtering>\n            </resource>\n        </resources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <configuration>\n                    <!-- Required because unit tests provides an own AcmeProvider\n                         which cannot be added to the module-info definition. -->\n                    <useModulePath>false</useModulePath>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.bitbucket.b_c</groupId>\n            <artifactId>jose4j</artifactId>\n            <version>${jose4j.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.bouncycastle</groupId>\n            <artifactId>bcprov-jdk18on</artifactId>\n            <version>${bouncycastle.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.bouncycastle</groupId>\n            <artifactId>bcpkix-jdk18on</artifactId>\n            <version>${bouncycastle.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <version>${slf4j.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-simple</artifactId>\n            <version>${slf4j.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.bouncycastle</groupId>\n            <artifactId>bcpg-jdk18on</artifactId>\n            <version>${bouncycastle.version}</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n</project>\n"
  },
  {
    "path": "acme4j-client/src/main/java/.gitignore",
    "content": ""
  },
  {
    "path": "acme4j-client/src/main/java/module-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This is the main module of the acme4j client.\n */\nmodule org.shredzone.acme4j {\n    requires static com.github.spotbugs.annotations;\n    requires java.net.http;\n    requires org.bouncycastle.pkix;\n    requires org.bouncycastle.provider;\n    requires org.jose4j;\n    requires org.slf4j;\n\n    exports org.shredzone.acme4j;\n    exports org.shredzone.acme4j.challenge;\n    exports org.shredzone.acme4j.connector;\n    exports org.shredzone.acme4j.exception;\n    exports org.shredzone.acme4j.provider;\n    exports org.shredzone.acme4j.toolbox;\n    exports org.shredzone.acme4j.util;\n\n    uses org.shredzone.acme4j.provider.AcmeProvider;\n    uses org.shredzone.acme4j.provider.ChallengeProvider;\n\n    provides org.shredzone.acme4j.provider.AcmeProvider\n            with org.shredzone.acme4j.provider.GenericAcmeProvider,\n                 org.shredzone.acme4j.provider.actalis.ActalisAcmeProvider,\n                 org.shredzone.acme4j.provider.google.GoogleAcmeProvider,\n                 org.shredzone.acme4j.provider.letsencrypt.LetsEncryptAcmeProvider,\n                 org.shredzone.acme4j.provider.pebble.PebbleAcmeProvider,\n                 org.shredzone.acme4j.provider.sslcom.SslComAcmeProvider,\n                 org.shredzone.acme4j.provider.zerossl.ZeroSSLAcmeProvider;\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/Account.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport java.io.Serial;\nimport java.net.URI;\nimport java.net.URL;\nimport java.security.KeyPair;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\n\nimport edu.umd.cs.findbugs.annotations.SuppressFBWarnings;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.connector.ResourceIterator;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeNotSupportedException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.exception.AcmeServerException;\nimport org.shredzone.acme4j.toolbox.AcmeUtils;\nimport org.shredzone.acme4j.toolbox.JSON.Value;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.JoseUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * A representation of an account at the ACME server.\n */\npublic class Account extends AcmeJsonResource {\n    @Serial\n    private static final long serialVersionUID = 7042863483428051319L;\n    private static final Logger LOG = LoggerFactory.getLogger(Account.class);\n\n    private static final String KEY_TOS_AGREED = \"termsOfServiceAgreed\";\n    private static final String KEY_ORDERS = \"orders\";\n    private static final String KEY_CONTACT = \"contact\";\n    private static final String KEY_STATUS = \"status\";\n    private static final String KEY_EXTERNAL_ACCOUNT_BINDING = \"externalAccountBinding\";\n\n    protected Account(Login login, URL location) {\n        super(login, location);\n    }\n\n    /**\n     * Returns if the user agreed to the terms of service.\n     *\n     * @return {@code true} if the user agreed to the terms of service. May be\n     *         empty if the server did not provide such an information.\n     */\n    public Optional<Boolean> getTermsOfServiceAgreed() {\n        return getJSON().get(KEY_TOS_AGREED).map(Value::asBoolean);\n    }\n\n    /**\n     * List of registered contact addresses (emails, phone numbers etc).\n     * <p>\n     * This list is unmodifiable. Use {@link #modify()} to change the contacts. May be\n     * empty, but is never {@code null}.\n     */\n    public List<URI> getContacts() {\n        return getJSON().get(KEY_CONTACT)\n                .asArray()\n                .stream()\n                .map(Value::asURI)\n                .toList();\n    }\n\n    /**\n     * Returns the current status of the account.\n     * <p>\n     * Possible values are: {@link Status#VALID}, {@link Status#DEACTIVATED},\n     * {@link Status#REVOKED}.\n     */\n    public Status getStatus() {\n        return getJSON().get(KEY_STATUS).asStatus();\n    }\n\n    /**\n     * Returns {@code true} if the account is bound to an external non-ACME account.\n     *\n     * @since 2.8\n     */\n    public boolean hasExternalAccountBinding() {\n        return getJSON().contains(KEY_EXTERNAL_ACCOUNT_BINDING);\n    }\n\n    /**\n     * Returns the key identifier of the external non-ACME account. If this account is\n     * not bound to an external account, the result is empty.\n     *\n     * @since 2.8\n     */\n    public Optional<String> getKeyIdentifier() {\n        return getJSON().get(KEY_EXTERNAL_ACCOUNT_BINDING)\n                .optional().map(Value::asObject)\n                .map(j -> j.get(\"protected\")).map(Value::asEncodedObject)\n                .map(j -> j.get(\"kid\")).map(Value::asString);\n    }\n\n    /**\n     * Returns an {@link Iterator} of all {@link Order} belonging to this\n     * {@link Account}.\n     * <p>\n     * Using the iterator will initiate one or more requests to the ACME server.\n     *\n     * @return {@link Iterator} instance that returns {@link Order} objects in no specific\n     * sorting order. {@link Iterator#hasNext()} and {@link Iterator#next()} may throw\n     * {@link AcmeProtocolException} if a batch of authorization URIs could not be fetched\n     * from the server.\n     */\n    public Iterator<Order> getOrders() {\n        var ordersUrl = getJSON().get(KEY_ORDERS).optional().map(Value::asURL);\n        if (ordersUrl.isEmpty()) {\n            // Let's Encrypt does not provide this field at the moment, although it's required.\n            // See https://github.com/letsencrypt/boulder/issues/3335\n            throw new AcmeNotSupportedException(\"getOrders()\");\n        }\n        return new ResourceIterator<>(getLogin(), KEY_ORDERS, ordersUrl.get(), Login::bindOrder);\n    }\n\n    /**\n     * Creates a builder for a new {@link Order}.\n     *\n     * @return {@link OrderBuilder} object\n     */\n    public OrderBuilder newOrder() {\n        return getLogin().newOrder();\n    }\n\n    /**\n     * Pre-authorizes a domain. The CA will check if it accepts the domain for\n     * certification, and returns the necessary challenges.\n     * <p>\n     * Some servers may not allow pre-authorization.\n     * <p>\n     * It is not possible to pre-authorize wildcard domains.\n     *\n     * @param domain\n     *            Domain name to be pre-authorized. IDN names are accepted and will be ACE\n     *            encoded automatically.\n     * @return {@link Authorization} object for this domain\n     * @throws AcmeException\n     *             if the server does not allow pre-authorization\n     * @throws AcmeServerException\n     *             if the server allows pre-authorization, but will refuse to issue a\n     *             certificate for this domain\n     */\n    public Authorization preAuthorizeDomain(String domain) throws AcmeException {\n        Objects.requireNonNull(domain, \"domain\");\n        if (domain.isEmpty()) {\n            throw new IllegalArgumentException(\"domain must not be empty\");\n        }\n        return preAuthorize(Identifier.dns(domain));\n    }\n\n    /**\n     * Pre-authorizes an {@link Identifier}. The CA will check if it accepts the\n     * identifier for certification, and returns the necessary challenges.\n     * <p>\n     * Some servers may not allow pre-authorization.\n     * <p>\n     * It is not possible to pre-authorize wildcard domains.\n     *\n     * @param identifier\n     *            {@link Identifier} to be pre-authorized.\n     * @return {@link Authorization} object for this identifier\n     * @throws AcmeException\n     *             if the server does not allow pre-authorization\n     * @throws AcmeServerException\n     *             if the server allows pre-authorization, but will refuse to issue a\n     *             certificate for this identifier\n     * @since 2.3\n     */\n    public Authorization preAuthorize(Identifier identifier) throws AcmeException {\n        Objects.requireNonNull(identifier, \"identifier\");\n\n        var newAuthzUrl = getSession().resourceUrl(Resource.NEW_AUTHZ);\n\n        if (identifier.toMap().containsKey(Identifier.KEY_SUBDOMAIN_AUTH_ALLOWED)\n                && !getSession().getMetadata().isSubdomainAuthAllowed()) {\n            throw new AcmeNotSupportedException(\"subdomain-auth\");\n        }\n\n        LOG.debug(\"preAuthorize {}\", identifier);\n        try (var conn = getSession().connect()) {\n            var claims = new JSONBuilder();\n            claims.put(\"identifier\", identifier.toMap());\n\n            conn.sendSignedRequest(newAuthzUrl, claims, getLogin());\n\n            var auth = getLogin().bindAuthorization(conn.getLocation());\n            auth.setJSON(conn.readJsonResponse());\n            return auth;\n        }\n    }\n\n    /**\n     * Changes the {@link KeyPair} associated with the account.\n     * <p>\n     * After a successful call, the new key pair is already set in the associated\n     * {@link Login}. The old key pair can be discarded.\n     *\n     * @param newKeyPair\n     *         new {@link KeyPair} to be used for identifying this account\n     */\n    public void changeKey(KeyPair newKeyPair) throws AcmeException {\n        Objects.requireNonNull(newKeyPair, \"newKeyPair\");\n        if (Arrays.equals(getLogin().getPublicKey().getEncoded(),\n                        newKeyPair.getPublic().getEncoded())) {\n            throw new IllegalArgumentException(\"newKeyPair must actually be a new key pair\");\n        }\n\n        LOG.debug(\"key-change\");\n\n        try (var conn = getSession().connect()) {\n            var keyChangeUrl = getSession().resourceUrl(Resource.KEY_CHANGE);\n\n            var payloadClaim = new JSONBuilder();\n            payloadClaim.put(\"account\", getLocation());\n            payloadClaim.putKey(\"oldKey\", getLogin().getPublicKey());\n\n            var jose = JoseUtils.createJoseRequest(keyChangeUrl, newKeyPair,\n                    payloadClaim, null, null);\n\n            conn.sendSignedRequest(keyChangeUrl, jose, getLogin());\n\n            getLogin().setKeyPair(newKeyPair);\n        }\n    }\n\n    /**\n     * Permanently deactivates an account. Related certificates may still be valid after\n     * account deactivation, and need to be revoked separately if neccessary.\n     * <p>\n     * A deactivated account cannot be reactivated!\n     */\n    public void deactivate() throws AcmeException {\n        LOG.debug(\"deactivate\");\n        try (var conn = getSession().connect()) {\n            var claims = new JSONBuilder();\n            claims.put(KEY_STATUS, \"deactivated\");\n\n            conn.sendSignedRequest(getLocation(), claims, getLogin());\n            setJSON(conn.readJsonResponse());\n        }\n    }\n\n    /**\n     * Modifies the account data of the account.\n     *\n     * @return {@link EditableAccount} where the account can be modified\n     */\n    public EditableAccount modify() {\n        return new EditableAccount();\n    }\n\n    /**\n     * Provides editable properties of an {@link Account}.\n     */\n    public class EditableAccount {\n        private final List<URI> editContacts = new ArrayList<>();\n\n        private EditableAccount() {\n            editContacts.addAll(Account.this.getContacts());\n        }\n\n        /**\n         * Returns the list of all contact URIs for modification. Use the {@link List}\n         * methods to modify the contact list.\n         * <p>\n         * The modified list is not validated. If you change entries, you have to make\n         * sure that they are valid according to the RFC. It is recommended to use\n         * the {@code addContact()} methods below to add new contacts to the list.\n         */\n        @SuppressFBWarnings(\"EI_EXPOSE_REP\")   // behavior is intended\n        public List<URI> getContacts() {\n            return editContacts;\n        }\n\n        /**\n         * Adds a new Contact to the account.\n         *\n         * @param contact\n         *            Contact URI\n         * @return itself\n         */\n        public EditableAccount addContact(URI contact) {\n            AcmeUtils.validateContact(contact);\n            editContacts.add(contact);\n            return this;\n        }\n\n        /**\n         * Adds a new Contact to the account.\n         * <p>\n         * This is a convenience call for {@link #addContact(URI)}.\n         *\n         * @param contact\n         *            Contact URI as string\n         * @return itself\n         */\n        public EditableAccount addContact(String contact) {\n            addContact(URI.create(contact));\n            return this;\n        }\n\n        /**\n         * Adds a new Contact email to the account.\n         * <p>\n         * This is a convenience call for {@link #addContact(String)} that doesn't\n         * require to prepend the email address with the \"mailto\" scheme.\n         *\n         * @param email\n         *            Contact email without \"mailto\" scheme (e.g. test@gmail.com)\n         * @return itself\n         */\n        public EditableAccount addEmail(String email) {\n            addContact(\"mailto:\" + email);\n            return this;\n        }\n\n        /**\n         * Commits the changes and updates the account.\n         */\n        public void commit() throws AcmeException {\n            LOG.debug(\"modify/commit\");\n            try (var conn = getSession().connect()) {\n                var claims = new JSONBuilder();\n                if (!editContacts.isEmpty()) {\n                    claims.put(KEY_CONTACT, editContacts);\n                }\n\n                conn.sendSignedRequest(getLocation(), claims, getLogin());\n                setJSON(conn.readJsonResponse());\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/AccountBuilder.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static java.util.Objects.requireNonNull;\nimport static org.jose4j.jws.AlgorithmIdentifiers.*;\nimport static org.shredzone.acme4j.toolbox.JoseUtils.macKeyAlgorithm;\n\nimport java.net.URI;\nimport java.security.KeyPair;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\n\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.SecretKeySpec;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.toolbox.AcmeUtils;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.JoseUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * A builder for registering a new account with the CA.\n * <p>\n * You need to create a new key pair and set it via {@link #useKeyPair(KeyPair)}. Your\n * account will be identified by the public part of that key pair, so make sure to store\n * it safely! There is no automatic way to regain access to your account if the key pair\n * is lost.\n * <p>\n * Depending on the CA you register with, you might need to give additional information.\n * <ul>\n *     <li>You might need to agree to the terms of service via\n *     {@link #agreeToTermsOfService()}.</li>\n *     <li>You might need to give at least one contact URI.</li>\n *     <li>You might need to provide a key identifier (e.g. your customer number) and\n *     a shared secret via {@link #withKeyIdentifier(String, SecretKey)}.</li>\n * </ul>\n * <p>\n * It is not possible to modify an existing account with the {@link AccountBuilder}. To\n * modify an existing account, use {@link Account#modify()} and\n * {@link Account#changeKey(KeyPair)}.\n */\npublic class AccountBuilder {\n    private static final Logger LOG = LoggerFactory.getLogger(AccountBuilder.class);\n    private static final Set<String> VALID_ALGORITHMS = Set.of(HMAC_SHA256, HMAC_SHA384, HMAC_SHA512);\n\n    private final List<URI> contacts = new ArrayList<>();\n    private @Nullable Boolean termsOfServiceAgreed;\n    private @Nullable Boolean onlyExisting;\n    private @Nullable String keyIdentifier;\n    private @Nullable KeyPair keyPair;\n    private @Nullable SecretKey macKey;\n    private @Nullable String macAlgorithm;\n\n    /**\n     * Add a contact URI to the list of contacts.\n     * <p>\n     * A contact URI may be e.g. an email address or a phone number. It depends on the CA\n     * what kind of contact URIs are accepted, and how many must be provided as minimum.\n     *\n     * @param contact\n     *         Contact URI\n     * @return itself\n     */\n    public AccountBuilder addContact(URI contact) {\n        AcmeUtils.validateContact(contact);\n        contacts.add(contact);\n        return this;\n    }\n\n    /**\n     * Add a contact address to the list of contacts.\n     * <p>\n     * This is a convenience call for {@link #addContact(URI)}.\n     *\n     * @param contact\n     *         Contact URI as string\n     * @return itself\n     * @throws IllegalArgumentException\n     *         if there is a syntax error in the URI string\n     */\n    public AccountBuilder addContact(String contact) {\n        addContact(URI.create(contact));\n        return this;\n    }\n\n    /**\n     * Add an email address to the list of contacts.\n     * <p>\n     * This is a convenience call for {@link #addContact(String)} that doesn't require\n     * to prepend the \"mailto\" scheme to an email address.\n     *\n     * @param email\n     *         Contact email without \"mailto\" scheme (e.g. test@gmail.com)\n     * @return itself\n     * @throws IllegalArgumentException\n     *         if there is a syntax error in the URI string\n     */\n    public AccountBuilder addEmail(String email) {\n        if (email.startsWith(\"mailto:\")) {\n            addContact(email);\n        } else {\n            addContact(\"mailto:\" + email);\n        }\n        return this;\n    }\n\n    /**\n     * Documents that the user has agreed to the terms of service.\n     * <p>\n     * If the CA requires the user to agree to the terms of service, it is your\n     * responsibility to present them to the user, and actively ask for their agreement. A\n     * link to the terms of service is provided via\n     * {@code session.getMetadata().getTermsOfService()}.\n     *\n     * @return itself\n     */\n    public AccountBuilder agreeToTermsOfService() {\n        this.termsOfServiceAgreed = true;\n        return this;\n    }\n\n    /**\n     * Signals that only an existing account should be returned. The server will not\n     * create a new account if the key is not known.\n     * <p>\n     * If you have lost your account's location URL, but still have your account's key\n     * pair, you can register your account again with the same key, and use\n     * {@link #onlyExisting()} to make sure that your existing account is returned. If\n     * your key is unknown to the server, an error is thrown once the account is to be\n     * created.\n     *\n     * @return itself\n     */\n    public AccountBuilder onlyExisting() {\n        this.onlyExisting = true;\n        return this;\n    }\n\n    /**\n     * Sets the {@link KeyPair} to be used for this account.\n     * <p>\n     * Only the public key of the pair is sent to the server for registration. acme4j will\n     * never send the private key part.\n     * <p>\n     * Make sure to store your key pair safely after registration! There is no automatic\n     * way to regain access to your account if the key pair is lost.\n     *\n     * @param keyPair\n     *         Account's {@link KeyPair}\n     * @return itself\n     */\n    public AccountBuilder useKeyPair(KeyPair keyPair) {\n        this.keyPair = requireNonNull(keyPair, \"keyPair\");\n        return this;\n    }\n\n    /**\n     * Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires\n     * an individual account identification (e.g. your customer number) and a shared\n     * secret for registration. See the documentation of your CA about how to retrieve the\n     * key identifier and MAC key.\n     *\n     * @param kid\n     *         Key Identifier\n     * @param macKey\n     *         MAC key\n     * @return itself\n     * @see #withKeyIdentifier(String, String)\n     */\n    public AccountBuilder withKeyIdentifier(String kid, SecretKey macKey) {\n        if (kid != null && kid.isEmpty()) {\n            throw new IllegalArgumentException(\"kid must not be empty\");\n        }\n        this.macKey = requireNonNull(macKey, \"macKey\");\n        this.keyIdentifier = kid;\n        return this;\n    }\n\n    /**\n     * Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires\n     * an individual account identification (e.g. your customer number) and a shared\n     * secret for registration. See the documentation of your CA about how to retrieve the\n     * key identifier and MAC key.\n     * <p>\n     * This is a convenience call of {@link #withKeyIdentifier(String, SecretKey)} that\n     * accepts a base64url encoded MAC key, so both parameters can be passed in as\n     * strings.\n     *\n     * @param kid\n     *         Key Identifier\n     * @param encodedMacKey\n     *         Base64url encoded MAC key.\n     * @return itself\n     * @see #withKeyIdentifier(String, SecretKey)\n     */\n    public AccountBuilder withKeyIdentifier(String kid, String encodedMacKey) {\n        var encodedKey = AcmeUtils.base64UrlDecode(requireNonNull(encodedMacKey, \"encodedMacKey\"));\n        return withKeyIdentifier(kid, new SecretKeySpec(encodedKey, \"HMAC\"));\n    }\n\n    /**\n     * Sets the MAC key algorithm that is provided by the CA. To be used in combination\n     * with key identifier. By default, the algorithm is deduced from the size of the\n     * MAC key. If a different size is needed, it can be set using this method.\n     *\n     * @param macAlgorithm\n     *         the algorithm to be set in the {@code alg} field, e.g. {@code \"HS512\"}.\n     * @return itself\n     * @since 3.1.0\n     */\n    public AccountBuilder withMacAlgorithm(String macAlgorithm) {\n        var algorithm = requireNonNull(macAlgorithm, \"macAlgorithm\");\n        if (!VALID_ALGORITHMS.contains(algorithm)) {\n            throw new IllegalArgumentException(\"Invalid MAC algorithm: \" + macAlgorithm);\n        }\n        this.macAlgorithm = algorithm;\n        return this;\n    }\n\n    /**\n     * Creates a new account.\n     * <p>\n     * Use this method to finally create your account with the given parameters. Do not\n     * use the {@link AccountBuilder} after invoking this method.\n     *\n     * @param session\n     *         {@link Session} to be used for registration\n     * @return {@link Account} referring to the new account\n     * @see #createLogin(Session)\n     */\n    public Account create(Session session) throws AcmeException {\n        return createLogin(session).getAccount();\n    }\n\n    /**\n     * Creates a new account.\n     * <p>\n     * This method is identical to {@link #create(Session)}, but returns a {@link Login}\n     * that is ready to be used.\n     *\n     * @param session\n     *         {@link Session} to be used for registration\n     * @return {@link Login} referring to the new account\n     */\n    public Login createLogin(Session session) throws AcmeException {\n        requireNonNull(session, \"session\");\n\n        if (keyPair == null) {\n            throw new IllegalStateException(\"Use AccountBuilder.useKeyPair() to set the account's key pair.\");\n        }\n\n        LOG.debug(\"create\");\n\n        try (var conn = session.connect()) {\n            var resourceUrl = session.resourceUrl(Resource.NEW_ACCOUNT);\n\n            var claims = new JSONBuilder();\n            if (!contacts.isEmpty()) {\n                claims.put(\"contact\", contacts);\n            }\n            if (termsOfServiceAgreed != null) {\n                claims.put(\"termsOfServiceAgreed\", termsOfServiceAgreed);\n            }\n            if (keyIdentifier != null && macKey != null) {\n                var algorithm = Optional.ofNullable(macAlgorithm)\n                        .or(session.provider()::getProposedEabMacAlgorithm)\n                        .orElseGet(() -> macKeyAlgorithm(macKey));\n                claims.put(\"externalAccountBinding\", JoseUtils.createExternalAccountBinding(\n                        keyIdentifier, keyPair.getPublic(), macKey, algorithm, resourceUrl));\n            }\n            if (onlyExisting != null) {\n                claims.put(\"onlyReturnExisting\", onlyExisting);\n            }\n\n            conn.sendSignedRequest(resourceUrl, claims, session, (url, payload, nonce) ->\n                    JoseUtils.createJoseRequest(url, keyPair, payload, nonce, null));\n\n            var login = new Login(conn.getLocation(), keyPair, session);\n            login.getAccount().setJSON(conn.readJsonResponse());\n            return login;\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/AcmeJsonResource.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport java.io.Serial;\nimport java.net.URL;\nimport java.time.Instant;\nimport java.util.Objects;\nimport java.util.Optional;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeLazyLoadingException;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * An extension of {@link AcmeResource} that also contains the current state of a resource\n * as JSON document. If the current state is not present, this class takes care of\n * fetching it from the server if necessary.\n */\npublic abstract class AcmeJsonResource extends AcmeResource {\n    @Serial\n    private static final long serialVersionUID = -5060364275766082345L;\n    private static final Logger LOG = LoggerFactory.getLogger(AcmeJsonResource.class);\n\n    private @Nullable JSON data = null;\n    private @Nullable Instant retryAfter = null;\n\n    /**\n     * Create a new {@link AcmeJsonResource}.\n     *\n     * @param login\n     *            {@link Login} the resource is bound with\n     * @param location\n     *            Location {@link URL} of this resource\n     */\n    protected AcmeJsonResource(Login login, URL location) {\n        super(login, location);\n    }\n\n    /**\n     * Returns the JSON representation of the resource data.\n     * <p>\n     * If there is no data, {@link #fetch()} is invoked to fetch it from the server.\n     * <p>\n     * This method can be used to read proprietary data from the resources.\n     *\n     * @return Resource data, as {@link JSON}.\n     * @throws AcmeLazyLoadingException\n     *         if an {@link AcmeException} occured while fetching the current state from\n     *         the server.\n     */\n    public JSON getJSON() {\n        if (data == null) {\n            try {\n                fetch();\n            } catch (AcmeException ex) {\n                throw new AcmeLazyLoadingException(this, ex);\n            }\n        }\n        return Objects.requireNonNull(data);\n    }\n\n    /**\n     * Sets the JSON representation of the resource data.\n     *\n     * @param data\n     *            New {@link JSON} data, must not be {@code null}.\n     */\n    protected void setJSON(JSON data) {\n        invalidate();\n        this.data = Objects.requireNonNull(data, \"data\");\n    }\n\n    /**\n     * Checks if this resource is valid.\n     *\n     * @return {@code true} if the resource state has been loaded from the server. If\n     *         {@code false}, {@link #getJSON()} would implicitly call {@link #fetch()}\n     *         to fetch the current state from the server.\n     */\n    protected boolean isValid() {\n        return data != null;\n    }\n\n    /**\n     * Invalidates the state of this resource. Enforces a {@link #fetch()} when\n     * {@link #getJSON()} is invoked.\n     * <p>\n     * Subclasses can override this method to purge internal caches that are based on the\n     * JSON structure. Remember to invoke {@code super.invalidate()}!\n     */\n    protected void invalidate() {\n        data = null;\n        retryAfter = null;\n    }\n\n    /**\n     * Updates this resource, by fetching the current resource data from the server.\n     *\n     * @return An {@link Optional} estimation when the resource status will change. If you\n     * are polling for the resource to complete, you should wait for the given instant\n     * before trying again. Empty if the server did not return a \"Retry-After\" header.\n     * @throws AcmeException\n     *         if the resource could not be fetched.\n     * @since 3.2.0\n     */\n    public Optional<Instant> fetch() throws AcmeException {\n        var resourceType = getClass().getSimpleName();\n        LOG.debug(\"update {}\", resourceType);\n        try (var conn = getSession().connect()) {\n            conn.sendSignedPostAsGetRequest(getLocation(), getLogin());\n            setJSON(conn.readJsonResponse());\n            var retryAfterOpt = conn.getRetryAfter();\n            retryAfterOpt.ifPresent(instant -> LOG.debug(\"Retry-After: {}\", instant));\n            setRetryAfter(retryAfterOpt.orElse(null));\n            return retryAfterOpt;\n        }\n    }\n\n    /**\n     * Sets a Retry-After instant.\n     *\n     * @since 3.2.0\n     */\n    protected void setRetryAfter(@Nullable Instant retryAfter) {\n        this.retryAfter = retryAfter;\n    }\n\n    /**\n     * Gets an estimation when the resource status will change. If you are polling for\n     * the resource to complete, you should wait for the given instant before trying\n     * a status refresh.\n     * <p>\n     * This instant was sent with the Retry-After header at the last update.\n     *\n     * @return Retry-after {@link Instant}, or empty if there was no such header.\n     * @since 3.2.0\n     */\n    public Optional<Instant> getRetryAfter() {\n        return Optional.ofNullable(retryAfter);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/AcmeResource.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.net.URL;\nimport java.util.Objects;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\n\n/**\n * This is the root class of all ACME resources (like accounts, orders, certificates).\n * Every resource is identified by its location URL.\n * <p>\n * This class also takes care for proper serialization and de-serialization of the\n * resource. After de-serialization, the resource must be bound to a {@link Login} again,\n * using {@link #rebind(Login)}.\n */\npublic abstract class AcmeResource implements Serializable {\n    @Serial\n    private static final long serialVersionUID = -7930580802257379731L;\n\n    private transient @Nullable Login login;\n    private final URL location;\n\n    /**\n     * Create a new {@link AcmeResource}.\n     *\n     * @param login\n     *            {@link Login} the resource is bound with\n     * @param location\n     *            Location {@link URL} of this resource\n     */\n    protected AcmeResource(Login login, URL location) {\n        this.location = Objects.requireNonNull(location, \"location\");\n        rebind(login);\n    }\n\n    /**\n     * Gets the {@link Login} this resource is bound with.\n     */\n    protected Login getLogin() {\n        if (login == null) {\n            throw new IllegalStateException(\"Use rebind() for binding this object to a login.\");\n        }\n        return login;\n    }\n\n    /**\n     * Gets the {@link Session} this resource is bound with.\n     */\n    protected Session getSession() {\n        return getLogin().getSession();\n    }\n\n    /**\n     * Rebinds this resource to a {@link Login}.\n     * <p>\n     * Logins are not serialized, because they contain volatile session data and also a\n     * private key. After de-serialization of an {@link AcmeResource}, use this method to\n     * rebind it to a {@link Login}.\n     *\n     * @param login\n     *            {@link Login} to bind this resource to\n     */\n    public void rebind(Login login) {\n        if (this.login != null) {\n            throw new IllegalStateException(\"Resource is already bound to a login\");\n        }\n        this.login = Objects.requireNonNull(login, \"login\");\n    }\n\n    /**\n     * Gets the resource's location.\n     */\n    public URL getLocation() {\n        return location;\n    }\n\n    @Override\n    protected final void finalize() {\n        // CT_CONSTRUCTOR_THROW: Prevents finalizer attack\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static java.util.stream.Collectors.toUnmodifiableList;\n\nimport java.io.Serial;\nimport java.net.URL;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.EnumSet;\nimport java.util.List;\nimport java.util.Optional;\n\nimport org.shredzone.acme4j.challenge.Challenge;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.AcmeUtils;\nimport org.shredzone.acme4j.toolbox.JSON.Value;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Represents an authorization request at the ACME server.\n */\npublic class Authorization extends AcmeJsonResource implements PollableResource {\n    @Serial\n    private static final long serialVersionUID = -3116928998379417741L;\n    private static final Logger LOG = LoggerFactory.getLogger(Authorization.class);\n\n    protected Authorization(Login login, URL location) {\n        super(login, location);\n    }\n\n    /**\n     * Gets the {@link Identifier} to be authorized.\n     * <p>\n     * For wildcard domain orders, the domain itself (without wildcard prefix) is returned\n     * here. To find out if this {@link Authorization} is related to a wildcard domain\n     * order, check the {@link #isWildcard()} method.\n     *\n     * @since 2.3\n     */\n    public Identifier getIdentifier() {\n        return getJSON().get(\"identifier\").asIdentifier();\n    }\n\n    /**\n     * Gets the authorization status.\n     * <p>\n     * Possible values are: {@link Status#PENDING}, {@link Status#VALID},\n     * {@link Status#INVALID}, {@link Status#DEACTIVATED}, {@link Status#EXPIRED},\n     * {@link Status#REVOKED}.\n     */\n    @Override\n    public Status getStatus() {\n        return getJSON().get(\"status\").asStatus();\n    }\n\n    /**\n     * Gets the expiry date of the authorization, if set by the server.\n     */\n    public Optional<Instant> getExpires() {\n        return getJSON().get(\"expires\")\n                    .map(Value::asString)\n                    .map(AcmeUtils::parseTimestamp);\n    }\n\n    /**\n     * Returns {@code true} if this {@link Authorization} is related to a wildcard domain,\n     * {@code false} otherwise.\n     */\n    public boolean isWildcard() {\n        return getJSON().get(\"wildcard\")\n                    .map(Value::asBoolean)\n                    .orElse(false);\n    }\n\n    /**\n     * Returns {@code true} if certificates for subdomains can be issued according to\n     * RFC9444.\n     *\n     * @since 3.3.0\n     */\n    public boolean isSubdomainAuthAllowed() {\n        return getJSON().get(\"subdomainAuthAllowed\")\n                .map(Value::asBoolean)\n                .orElse(false);\n    }\n\n    /**\n     * Gets a list of all challenges offered by the server, in no specific order.\n     */\n    public List<Challenge> getChallenges() {\n        var login = getLogin();\n\n        return getJSON().get(\"challenges\")\n                .asArray()\n                .stream()\n                .map(Value::asObject)\n                .map(login::createChallenge)\n                .collect(toUnmodifiableList());\n    }\n\n    /**\n     * Finds a {@link Challenge} of the given type. Responding to this {@link Challenge}\n     * is sufficient for authorization.\n     * <p>\n     * {@link Authorization#findChallenge(Class)} should be preferred, as this variant\n     * is not type safe.\n     *\n     * @param type\n     *            Challenge name (e.g. \"http-01\")\n     * @return {@link Challenge} matching that name, or empty if there is no such\n     *         challenge, or if the challenge alone is not sufficient for authorization.\n     * @throws ClassCastException\n     *             if the type does not match the expected Challenge class type\n     */\n    @SuppressWarnings(\"unchecked\")\n    public <T extends Challenge> Optional<T> findChallenge(final String type) {\n        return (Optional<T>) getChallenges().stream()\n                .filter(ch -> type.equals(ch.getType()))\n                .reduce((a, b) -> {\n                    throw new AcmeProtocolException(\"Found more than one challenge of type \" + type);\n                });\n    }\n\n    /**\n     * Finds a {@link Challenge} of the given class type. Responding to this {@link\n     * Challenge} is sufficient for authorization.\n     *\n     * @param type\n     *         Challenge type (e.g. \"Http01Challenge.class\")\n     * @return {@link Challenge} of that type, or empty if there is no such\n     * challenge, or if the challenge alone is not sufficient for authorization.\n     * @since 2.8\n     */\n    public <T extends Challenge> Optional<T> findChallenge(Class<T> type) {\n        return getChallenges().stream()\n                .filter(type::isInstance)\n                .map(type::cast)\n                .reduce((a, b) -> {\n                    throw new AcmeProtocolException(\"Found more than one challenge of type \" + type.getName());\n                });\n    }\n\n    /**\n     * Waits until the authorization is completed.\n     * <p>\n     * It is completed if it reaches either {@link Status#VALID} or\n     * {@link Status#INVALID}.\n     * <p>\n     * This method is synchronous and blocks the current thread.\n     *\n     * @param timeout\n     *         Timeout until a terminal status must have been reached\n     * @return Status that was reached\n     * @since 3.4.0\n     */\n    public Status waitForCompletion(Duration timeout)\n            throws AcmeException, InterruptedException {\n        return waitForStatus(EnumSet.of(Status.VALID, Status.INVALID), timeout);\n    }\n\n    /**\n     * Permanently deactivates the {@link Authorization}.\n     */\n    public void deactivate() throws AcmeException {\n        LOG.debug(\"deactivate\");\n        try (var conn = getSession().connect()) {\n            var claims = new JSONBuilder();\n            claims.put(\"status\", \"deactivated\");\n\n            conn.sendSignedRequest(getLocation(), claims, getLogin());\n            setJSON(conn.readJsonResponse());\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static java.util.Collections.unmodifiableList;\nimport static java.util.Objects.requireNonNull;\nimport static java.util.stream.Collectors.toList;\nimport static java.util.stream.Collectors.toUnmodifiableList;\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.getRenewalUniqueIdentifier;\n\nimport java.io.IOException;\nimport java.io.Serial;\nimport java.io.Writer;\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.security.KeyPair;\nimport java.security.Principal;\nimport java.security.cert.CertificateEncodingException;\nimport java.security.cert.X509Certificate;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Optional;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport edu.umd.cs.findbugs.annotations.SuppressFBWarnings;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeLazyLoadingException;\nimport org.shredzone.acme4j.exception.AcmeNotSupportedException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.AcmeUtils;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.JoseUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Represents an issued certificate and its certificate chain.\n * <p>\n * A certificate is immutable once it is issued. For renewal, a new certificate must be\n * ordered.\n */\npublic class Certificate extends AcmeResource {\n    @Serial\n    private static final long serialVersionUID = 7381527770159084201L;\n    private static final Logger LOG = LoggerFactory.getLogger(Certificate.class);\n\n    private @Nullable List<X509Certificate> certChain;\n    private @Nullable Collection<URL> alternates;\n    private transient @Nullable RenewalInfo renewalInfo = null;\n    private transient @Nullable List<Certificate> alternateCerts = null;\n\n    protected Certificate(Login login, URL certUrl) {\n        super(login, certUrl);\n    }\n\n    /**\n     * Downloads the certificate chain.\n     * <p>\n     * The certificate is downloaded lazily by the other methods. Usually there is no need\n     * to invoke this method, unless the download is to be enforced. If the certificate\n     * has been downloaded already, nothing will happen.\n     *\n     * @throws AcmeException\n     *         if the certificate could not be downloaded\n     */\n    public void download() throws AcmeException {\n        if (certChain == null) {\n            LOG.debug(\"download\");\n            try (var conn = getSession().connect()) {\n                conn.sendCertificateRequest(getLocation(), getLogin());\n                alternates = conn.getLinks(\"alternate\");\n                certChain = conn.readCertificates();\n            }\n        }\n    }\n\n    /**\n     * Returns the created certificate.\n     *\n     * @return The created end-entity {@link X509Certificate} without issuer chain.\n     */\n    public X509Certificate getCertificate() {\n        lazyDownload();\n        return requireNonNull(certChain).get(0);\n    }\n\n    /**\n     * Returns the created certificate and issuer chain.\n     *\n     * @return The created end-entity {@link X509Certificate} and issuer chain. The first\n     *         certificate is always the end-entity certificate, followed by the\n     *         intermediate certificates required to build a path to a trusted root.\n     */\n    public List<X509Certificate> getCertificateChain() {\n        lazyDownload();\n        return unmodifiableList(requireNonNull(certChain));\n    }\n\n    /**\n     * Returns URLs to alternate certificate chains.\n     *\n     * @return Alternate certificate chains, or empty if there are none.\n     */\n    public List<URL> getAlternates() {\n        lazyDownload();\n        return requireNonNull(alternates).stream().collect(toUnmodifiableList());\n    }\n\n    /**\n     * Returns alternate certificate chains, if available.\n     *\n     * @return Alternate certificate chains, or empty if there are none.\n     * @since 2.11\n     */\n    public List<Certificate> getAlternateCertificates() {\n        if (alternateCerts == null) {\n            var login = getLogin();\n            alternateCerts = getAlternates().stream()\n                    .map(login::bindCertificate)\n                    .collect(toList());\n        }\n        return unmodifiableList(alternateCerts);\n    }\n\n    /**\n     * Checks if this certificate was issued by the given issuer name.\n     *\n     * @param issuer\n     *         Issuer name to check against, case-sensitive\n     * @return {@code true} if this issuer name was found in the certificate chain as\n     * issuer, {@code false} otherwise.\n     * @since 3.0.0\n     */\n    public boolean isIssuedBy(String issuer) {\n        var issuerCn = \"CN=\" + issuer;\n        return getCertificateChain().stream()\n                .map(X509Certificate::getIssuerX500Principal)\n                .map(Principal::getName)\n                .anyMatch(issuerCn::equals);\n    }\n\n    /**\n     * Finds a {@link Certificate} that was issued by the given issuer name.\n     *\n     * @param issuer\n     *         Issuer name to check against, case-sensitive\n     * @return Certificate that was issued by that issuer, or {@code empty} if there was\n     * none. The returned {@link Certificate} may be this instance, or one of the\n     * {@link #getAlternateCertificates()} instances. If multiple certificates are issued\n     * by that issuer, the first one that was found is returned.\n     * @since 3.0.0\n     */\n    public Optional<Certificate> findCertificate(String issuer) {\n        if (isIssuedBy(issuer)) {\n            return Optional.of(this);\n        }\n        return getAlternateCertificates().stream()\n                .filter(c -> c.isIssuedBy(issuer))\n                .findFirst();\n    }\n\n    /**\n     * Writes the certificate to the given writer. It is written in PEM format, with the\n     * end-entity cert coming first, followed by the intermediate certificates.\n     *\n     * @param out\n     *            {@link Writer} to write to. The writer is not closed after use.\n     */\n    public void writeCertificate(Writer out) throws IOException {\n        try {\n            for (var cert : getCertificateChain()) {\n                AcmeUtils.writeToPem(cert.getEncoded(), AcmeUtils.PemLabel.CERTIFICATE, out);\n            }\n        } catch (CertificateEncodingException ex) {\n            throw new IOException(\"Encoding error\", ex);\n        }\n    }\n\n    /**\n     * Returns the location of the certificate's RenewalInfo. Empty if the CA does not\n     * provide this information.\n     *\n     * @since 3.0.0\n     */\n    public Optional<URL> getRenewalInfoLocation() {\n        try {\n            return getSession().resourceUrlOptional(Resource.RENEWAL_INFO)\n                    .map(baseUrl -> {\n                        try {\n                            var url = baseUrl.toExternalForm();\n                            if (!url.endsWith(\"/\")) {\n                                url += '/';\n                            }\n                            url += getRenewalUniqueIdentifier(getCertificate());\n                            return URI.create(url).toURL();\n                        } catch (MalformedURLException ex) {\n                            throw new AcmeProtocolException(\"Invalid RenewalInfo URL\", ex);\n                        }\n                    });\n        } catch (AcmeException ex) {\n            throw new AcmeLazyLoadingException(this, ex);\n        }\n    }\n\n    /**\n     * Returns {@code true} if the CA provides renewal information.\n     *\n     * @since 3.0.0\n     */\n    public boolean hasRenewalInfo() {\n        return getRenewalInfoLocation().isPresent();\n    }\n\n    /**\n     * Reads the RenewalInfo for this certificate.\n     *\n     * @return The {@link RenewalInfo} of this certificate.\n     * @throws AcmeNotSupportedException if the CA does not support renewal information.\n     * @since 3.0.0\n     */\n    @SuppressFBWarnings(\"EI_EXPOSE_REP\")   // behavior is intended\n    public RenewalInfo getRenewalInfo() {\n        if (renewalInfo == null) {\n            renewalInfo = getRenewalInfoLocation()\n                    .map(getLogin()::bindRenewalInfo)\n                    .orElseThrow(() -> new AcmeNotSupportedException(\"renewal-info\"));\n        }\n        return renewalInfo;\n    }\n\n    /**\n     * Revokes this certificate.\n     */\n    public void revoke() throws AcmeException {\n        revoke(null);\n    }\n\n    /**\n     * Revokes this certificate.\n     *\n     * @param reason\n     *            {@link RevocationReason} stating the reason of the revocation that is\n     *            used when generating OCSP responses and CRLs. {@code null} to give no\n     *            reason.\n     * @see #revoke(Login, X509Certificate, RevocationReason)\n     * @see #revoke(Session, KeyPair, X509Certificate, RevocationReason)\n     */\n    public void revoke(@Nullable RevocationReason reason) throws AcmeException {\n        revoke(getLogin(), getCertificate(), reason);\n    }\n\n    /**\n     * Revoke a certificate.\n     * <p>\n     * Use this method if the certificate's location is unknown, so you cannot regenerate\n     * a {@link Certificate} instance. This method requires a {@link Login} to your\n     * account and the issued certificate.\n     *\n     * @param login\n     *         {@link Login} to the account\n     * @param cert\n     *         The {@link X509Certificate} to be revoked\n     * @param reason\n     *         {@link RevocationReason} stating the reason of the revocation that is used\n     *         when generating OCSP responses and CRLs. {@code null} to give no reason.\n     * @see #revoke(Session, KeyPair, X509Certificate, RevocationReason)\n     * @since 2.6\n     */\n    public static void revoke(Login login, X509Certificate cert, @Nullable RevocationReason reason)\n                throws AcmeException {\n        LOG.debug(\"revoke\");\n\n        var session = login.getSession();\n\n        var resUrl = session.resourceUrl(Resource.REVOKE_CERT);\n\n        try (var conn = session.connect()) {\n            var claims = new JSONBuilder();\n            claims.putBase64(\"certificate\", cert.getEncoded());\n            if (reason != null) {\n                claims.put(\"reason\", reason.getReasonCode());\n            }\n\n            conn.sendSignedRequest(resUrl, claims, login);\n        } catch (CertificateEncodingException ex) {\n            throw new AcmeProtocolException(\"Invalid certificate\", ex);\n        }\n    }\n\n    /**\n     * Revoke a certificate.\n     * <p>\n     * Use this method if the key pair of your account was lost (so you are unable to\n     * login into your account), but you still have the key pair of the affected domain\n     * and the issued certificate.\n     *\n     * @param session\n     *         {@link Session} connected to the ACME server\n     * @param domainKeyPair\n     *         Key pair the CSR was signed with\n     * @param cert\n     *         The {@link X509Certificate} to be revoked\n     * @param reason\n     *         {@link RevocationReason} stating the reason of the revocation that is used\n     *         when generating OCSP responses and CRLs. {@code null} to give no reason.\n     * @see #revoke(Login, X509Certificate, RevocationReason)\n     */\n    public static void revoke(Session session, KeyPair domainKeyPair, X509Certificate cert,\n            @Nullable RevocationReason reason) throws AcmeException {\n        LOG.debug(\"revoke using the domain key pair\");\n\n        var resUrl = session.resourceUrl(Resource.REVOKE_CERT);\n\n        try (var conn = session.connect()) {\n            var claims = new JSONBuilder();\n            claims.putBase64(\"certificate\", cert.getEncoded());\n            if (reason != null) {\n                claims.put(\"reason\", reason.getReasonCode());\n            }\n\n            conn.sendSignedRequest(resUrl, claims, session, (url, payload, nonce) ->\n                    JoseUtils.createJoseRequest(url, domainKeyPair, payload, nonce, null));\n\n        } catch (CertificateEncodingException ex) {\n            throw new AcmeProtocolException(\"Invalid certificate\", ex);\n        }\n    }\n\n    /**\n     * Lazily downloads the certificate. Throws a runtime {@link AcmeLazyLoadingException}\n     * if the download failed.\n     */\n    private void lazyDownload() {\n        try {\n            download();\n        } catch (AcmeException ex) {\n            throw new AcmeLazyLoadingException(this, ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/Identifier.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static java.util.Collections.unmodifiableMap;\nimport static java.util.Objects.requireNonNull;\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.toAce;\n\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.net.InetAddress;\nimport java.net.UnknownHostException;\nimport java.util.Map;\nimport java.util.TreeMap;\n\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * Represents an identifier.\n * <p>\n * The ACME protocol only defines the DNS identifier, which identifies a domain name.\n * acme4j also supports IP identifiers.\n * <p>\n * CAs, and other acme4j modules, may define further, proprietary identifier types.\n *\n * @since 2.3\n */\npublic class Identifier implements Serializable {\n    @Serial\n    private static final long serialVersionUID = -7777851842076362412L;\n\n    /**\n     * Type constant for DNS identifiers.\n     */\n    public static final String TYPE_DNS = \"dns\";\n\n    /**\n     * Type constant for IP identifiers.\n     *\n     * @see <a href=\"https://tools.ietf.org/html/rfc8738\">RFC 8738</a>\n     */\n    public static final String TYPE_IP = \"ip\";\n\n    static final String KEY_TYPE = \"type\";\n    static final String KEY_VALUE = \"value\";\n    static final String KEY_ANCESTOR_DOMAIN = \"ancestorDomain\";\n    static final String KEY_SUBDOMAIN_AUTH_ALLOWED = \"subdomainAuthAllowed\";\n\n    private final Map<String, Object> content = new TreeMap<>();\n\n    /**\n     * Creates a new {@link Identifier}.\n     * <p>\n     * This is a generic constructor for identifiers. Refer to the documentation of your\n     * CA to find out about the accepted identifier types and values.\n     * <p>\n     * Note that for DNS identifiers, no ASCII encoding of unicode domain takes place\n     * here. Use {@link #dns(String)} instead.\n     *\n     * @param type\n     *            Identifier type\n     * @param value\n     *            Identifier value\n     */\n    public Identifier(String type, String value) {\n        content.put(KEY_TYPE, requireNonNull(type, KEY_TYPE));\n        content.put(KEY_VALUE, requireNonNull(value, KEY_VALUE));\n    }\n\n    /**\n     * Creates a new {@link Identifier} from the given {@link JSON} structure.\n     *\n     * @param json\n     *            {@link JSON} containing the identifier data\n     */\n    public Identifier(JSON json) {\n        if (!json.contains(KEY_TYPE)) {\n            throw new AcmeProtocolException(\"Required key \" + KEY_TYPE + \" is missing\");\n        }\n        if (!json.contains(KEY_VALUE)) {\n            throw new AcmeProtocolException(\"Required key \" + KEY_VALUE + \" is missing\");\n        }\n        content.putAll(json.toMap());\n    }\n\n    /**\n     * Makes a copy of the given Identifier.\n     */\n    private Identifier(Identifier identifier) {\n        content.putAll(identifier.content);\n    }\n\n    /**\n     * Creates a new DNS identifier for the given domain name.\n     *\n     * @param domain\n     *            Domain name. Unicode domains are automatically ASCII encoded.\n     * @return New {@link Identifier}\n     */\n    public static Identifier dns(String domain) {\n        return new Identifier(TYPE_DNS, toAce(domain));\n    }\n\n    /**\n     * Creates a new IP identifier for the given {@link InetAddress}.\n     *\n     * @param ip\n     *            {@link InetAddress}\n     * @return New {@link Identifier}\n     */\n    public static Identifier ip(InetAddress ip) {\n        return new Identifier(TYPE_IP, ip.getHostAddress());\n    }\n\n    /**\n     * Creates a new IP identifier for the given {@link InetAddress}.\n     *\n     * @param ip\n     *            IP address as {@link String}\n     * @return New {@link Identifier}\n     * @since 2.7\n     */\n    public static Identifier ip(String ip) {\n        try {\n            return ip(InetAddress.getByName(ip));\n        } catch (UnknownHostException ex) {\n            throw new IllegalArgumentException(\"Bad IP: \" + ip, ex);\n        }\n    }\n\n    /**\n     * Sets an ancestor domain, as required in RFC-9444.\n     *\n     * @param domain\n     *         The ancestor domain to be set. Unicode domains are automatically ASCII\n     *         encoded.\n     * @return An {@link Identifier} that contains the ancestor domain.\n     * @since 3.3.0\n     */\n    public Identifier withAncestorDomain(String domain) {\n        expectType(TYPE_DNS);\n\n        var result = new Identifier(this);\n        result.content.put(KEY_ANCESTOR_DOMAIN, toAce(domain));\n        return result;\n    }\n\n    /**\n     * Gives the permission to authorize subdomains of this domain, as required in\n     * RFC-9444.\n     *\n     * @return An {@link Identifier} that allows subdomain auths.\n     * @since 3.3.0\n     */\n    public Identifier allowSubdomainAuth() {\n        expectType(TYPE_DNS);\n\n        var result = new Identifier(this);\n        result.content.put(KEY_SUBDOMAIN_AUTH_ALLOWED, true);\n        return result;\n    }\n\n    /**\n     * Returns the identifier type.\n     */\n    public String getType() {\n        return content.get(KEY_TYPE).toString();\n    }\n\n    /**\n     * Returns the identifier value.\n     */\n    public String getValue() {\n        return content.get(KEY_VALUE).toString();\n    }\n\n    /**\n     * Returns the domain name if this is a DNS identifier.\n     *\n     * @return Domain name. Unicode domains are ASCII encoded.\n     * @throws AcmeProtocolException\n     *             if this is not a DNS identifier.\n     */\n    public String getDomain() {\n        expectType(TYPE_DNS);\n        return getValue();\n    }\n\n    /**\n     * Returns the IP address if this is an IP identifier.\n     *\n     * @return {@link InetAddress}\n     * @throws AcmeProtocolException\n     *             if this is not a DNS identifier.\n     */\n    public InetAddress getIP() {\n        expectType(TYPE_IP);\n        try {\n            return InetAddress.getByName(getValue());\n        } catch (UnknownHostException ex) {\n            throw new AcmeProtocolException(\"bad ip identifier value\", ex);\n        }\n    }\n\n    /**\n     * Returns the identifier as JSON map.\n     */\n    public Map<String, Object> toMap() {\n        return unmodifiableMap(content);\n    }\n\n    /**\n     * Makes sure this identifier is of the given type.\n     *\n     * @param type\n     *         Expected type\n     * @throws AcmeProtocolException\n     *         if this identifier is of a different type\n     */\n    private void expectType(String type) {\n        if (!type.equals(getType())) {\n            throw new AcmeProtocolException(\"expected '\" + type + \"' identifier, but found '\" + getType() + \"'\");\n        }\n    }\n\n    @Override\n    public String toString() {\n        if (content.size() == 2) {\n            return getType() + '=' + getValue();\n        }\n        return content.toString();\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (!(obj instanceof Identifier i)) {\n            return false;\n        }\n        return content.equals(i.content);\n    }\n\n    @Override\n    public int hashCode() {\n        return content.hashCode();\n    }\n\n    @Override\n    protected final void finalize() {\n        // CT_CONSTRUCTOR_THROW: Prevents finalizer attack\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/Login.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static java.util.Objects.requireNonNull;\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.getRenewalUniqueIdentifier;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.security.KeyPair;\nimport java.security.PublicKey;\nimport java.security.cert.X509Certificate;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport edu.umd.cs.findbugs.annotations.SuppressFBWarnings;\nimport org.shredzone.acme4j.challenge.Challenge;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeLazyLoadingException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.JoseUtils;\n\n/**\n * A {@link Login} into an account.\n * <p>\n * A login is bound to a {@link Session}. However, a {@link Session} can handle multiple\n * logins in parallel.\n * <p>\n * To create a login, you need to specify the location URI of the {@link Account}, and\n * need to provide the {@link KeyPair} the account was created with. If the account's\n * location URL is unknown, the account can be re-registered with the\n * {@link AccountBuilder}, using {@link AccountBuilder#onlyExisting()} to make sure that\n * no new account will be created. If the key pair was lost though, there is no automatic\n * way to regain access to your account, and you have to contact your CA's support hotline\n * for assistance.\n * <p>\n * Note that {@link Login} objects are intentionally not serializable, as they contain a\n * keypair and volatile data. On distributed systems, you can create a {@link Login} to\n * the same account for every service instance.\n */\npublic class Login {\n\n    private final Session session;\n    private final Account account;\n    private KeyPair keyPair;\n\n    /**\n     * Creates a new {@link Login}.\n     *\n     * @param accountLocation\n     *            Account location {@link URL}\n     * @param keyPair\n     *            {@link KeyPair} of the account\n     * @param session\n     *            {@link Session} to be used\n     */\n    public Login(URL accountLocation, KeyPair keyPair, Session session) {\n        this.keyPair = requireNonNull(keyPair, \"keyPair\");\n        this.session = requireNonNull(session, \"session\");\n        this.account = new Account(this, requireNonNull(accountLocation, \"accountLocation\"));\n    }\n\n    /**\n     * Gets the {@link Session} that is used.\n     */\n    @SuppressFBWarnings(\"EI_EXPOSE_REP\")    // behavior is intended\n    public Session getSession() {\n        return session;\n    }\n\n    /**\n     * Gets the {@link PublicKey} of the ACME account.\n     *\n     * @since 5.0.0\n     */\n    public PublicKey getPublicKey() {\n        return keyPair.getPublic();\n    }\n\n    /**\n     * Gets the {@link Account} that is bound to this login.\n     *\n     * @return {@link Account} bound to the login\n     */\n    @SuppressFBWarnings(\"EI_EXPOSE_REP\")    // behavior is intended\n    public Account getAccount() {\n        return account;\n    }\n\n    /**\n     * Creates a new instance of an existing {@link Authorization} and binds it to this\n     * login.\n     *\n     * @param location\n     *         Location of the Authorization\n     * @return {@link Authorization} bound to the login\n     */\n    public Authorization bindAuthorization(URL location) {\n        return new Authorization(this, requireNonNull(location, \"location\"));\n    }\n\n    /**\n     * Creates a new instance of an existing {@link Certificate} and binds it to this\n     * login.\n     *\n     * @param location\n     *         Location of the Certificate\n     * @return {@link Certificate} bound to the login\n     */\n    public Certificate bindCertificate(URL location) {\n        return new Certificate(this, requireNonNull(location, \"location\"));\n    }\n\n    /**\n     * Creates a new instance of an existing {@link Order} and binds it to this login.\n     *\n     * @param location\n     *         Location URL of the order\n     * @return {@link Order} bound to the login\n     */\n    public Order bindOrder(URL location) {\n        return new Order(this, requireNonNull(location, \"location\"));\n    }\n\n    /**\n     * Creates a new instance of an existing {@link RenewalInfo} and binds it to this\n     * login.\n     *\n     * @param location\n     *         Location URL of the renewal info\n     * @return {@link RenewalInfo} bound to the login\n     * @since 3.0.0\n     */\n    public RenewalInfo bindRenewalInfo(URL location) {\n        return new RenewalInfo(this, requireNonNull(location, \"location\"));\n    }\n\n    /**\n     * Creates a new instance of an existing {@link RenewalInfo} and binds it to this\n     * login.\n     *\n     * @param certificate\n     *         {@link X509Certificate} to get the {@link RenewalInfo} for\n     * @return {@link RenewalInfo} bound to the login\n     * @since 3.2.0\n     */\n    public RenewalInfo bindRenewalInfo(X509Certificate certificate) throws AcmeException {\n        try {\n            var url = getSession().resourceUrl(Resource.RENEWAL_INFO).toExternalForm();\n            if (!url.endsWith(\"/\")) {\n                url += '/';\n            }\n            url += getRenewalUniqueIdentifier(certificate);\n            return bindRenewalInfo(URI.create(url).toURL());\n        } catch (MalformedURLException ex) {\n            throw new AcmeProtocolException(\"Invalid RenewalInfo URL\", ex);\n        }\n    }\n\n    /**\n     * Creates a new instance of an existing {@link Challenge} and binds it to this\n     * login. Use this method only if the resulting challenge type is unknown.\n     *\n     * @param location\n     *         Location URL of the challenge\n     * @return {@link Challenge} bound to the login\n     * @since 2.8\n     * @see #bindChallenge(URL, Class)\n     */\n    public Challenge bindChallenge(URL location) {\n        try (var connect = session.connect()) {\n            connect.sendSignedPostAsGetRequest(location, this);\n            return createChallenge(connect.readJsonResponse());\n        } catch (AcmeException ex) {\n            throw new AcmeLazyLoadingException(Challenge.class, location, ex);\n        }\n    }\n\n    /**\n     * Creates a new instance of an existing {@link Challenge} and binds it to this\n     * login. Use this method if the resulting challenge type is known.\n     *\n     * @param location\n     *         Location URL of the challenge\n     * @param type\n     *         Expected challenge type\n     * @return Challenge bound to the login\n     * @throws AcmeProtocolException\n     *         if the challenge found at the location does not match the expected\n     *         challenge type.\n     * @since 2.12\n     */\n    public <C extends Challenge> C bindChallenge(URL location, Class<C> type) {\n        var challenge = bindChallenge(location);\n        if (!type.isInstance(challenge)) {\n            throw new AcmeProtocolException(\"Challenge type \" + challenge.getType()\n                    + \" does not match requested class \" + type);\n        }\n        return type.cast(challenge);\n    }\n\n    /**\n     * Creates a {@link Challenge} instance for the given challenge data.\n     *\n     * @param data\n     *            Challenge JSON data\n     * @return {@link Challenge} instance\n     */\n    public Challenge createChallenge(JSON data) {\n        var challenge = session.provider().createChallenge(this, data);\n        if (challenge == null) {\n            throw new AcmeProtocolException(\"Could not create challenge for: \" + data);\n        }\n        return challenge;\n    }\n\n    /**\n     * Creates a builder for a new {@link Order}.\n     *\n     * @return {@link OrderBuilder} object\n     * @since 3.0.0\n     */\n    public OrderBuilder newOrder() {\n        return new OrderBuilder(this);\n    }\n\n    /**\n     * Sets a different {@link KeyPair}. The new key pair is only used locally in this\n     * instance, but is not set on server side!\n     */\n    protected void setKeyPair(KeyPair keyPair) {\n        this.keyPair = requireNonNull(keyPair, \"keyPair\");\n    }\n\n    /**\n     * Creates an ACME JOSE request. This method is meant for internal purposes only.\n     *\n     * @param url\n     *         {@link URL} of the ACME call\n     * @param payload\n     *         ACME JSON payload. If {@code null}, a POST-as-GET request is generated\n     *         instead.\n     * @param nonce\n     *         Nonce to be used. {@code null} if no nonce is to be used in the JOSE\n     *         header.\n     * @return JSON structure of the JOSE request, ready to be sent.\n     * @since 5.0.0\n     */\n    public JSONBuilder createJoseRequest(URL url, @Nullable JSONBuilder payload, @Nullable String nonce) {\n        return JoseUtils.createJoseRequest(url, keyPair, payload, nonce, getAccount().getLocation().toString());\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static java.util.stream.Collectors.toList;\n\nimport java.net.URI;\nimport java.net.URL;\nimport java.time.Duration;\nimport java.util.Collection;\nimport java.util.Optional;\nimport java.util.Set;\n\nimport org.shredzone.acme4j.exception.AcmeNotSupportedException;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSON.Value;\n\n/**\n * A collection of metadata related to the CA provider.\n */\npublic class Metadata {\n\n    private final JSON meta;\n\n    /**\n     * Creates a new {@link Metadata} instance.\n     *\n     * @param meta\n     *            JSON map of metadata\n     */\n    public Metadata(JSON meta) {\n        this.meta = meta;\n    }\n\n    /**\n     * Returns an {@link URI} of the current terms of service, or empty if not available.\n     */\n    public Optional<URI> getTermsOfService() {\n        return meta.get(\"termsOfService\").map(Value::asURI);\n    }\n\n    /**\n     * Returns an {@link URL} of a website providing more information about the ACME\n     * server. Empty if not available.\n     */\n    public Optional<URL> getWebsite() {\n        return meta.get(\"website\").map(Value::asURL);\n    }\n\n    /**\n     * Returns a collection of hostnames, which the ACME server recognises as referring to\n     * itself for the purposes of CAA record validation. Empty if not available.\n     */\n    public Collection<String> getCaaIdentities() {\n        return meta.get(\"caaIdentities\")\n                .asArray()\n                .stream()\n                .map(Value::asString)\n                .collect(toList());\n    }\n\n    /**\n     * Returns whether an external account is required by this CA.\n     */\n    public boolean isExternalAccountRequired() {\n        return meta.get(\"externalAccountRequired\").map(Value::asBoolean).orElse(false);\n    }\n\n    /**\n     * Returns whether the CA supports short-term auto-renewal of certificates.\n     *\n     * @since 2.3\n     */\n    public boolean isAutoRenewalEnabled() {\n        return meta.get(\"auto-renewal\").isPresent();\n    }\n\n    /**\n     * Returns the minimum acceptable value for the maximum validity of a certificate\n     * before auto-renewal.\n     *\n     * @since 2.3\n     * @throws AcmeNotSupportedException if the server does not support auto-renewal.\n     */\n    public Duration getAutoRenewalMinLifetime() {\n        return meta.getFeature(\"auto-renewal\")\n                .map(Value::asObject)\n                .orElseGet(JSON::empty)\n                .get(\"min-lifetime\")\n                .asDuration();\n    }\n\n    /**\n     * Returns the maximum delta between auto-renewal end date and auto-renewal start\n     * date.\n     *\n     * @since 2.3\n     * @throws AcmeNotSupportedException if the server does not support auto-renewal.\n     */\n    public Duration getAutoRenewalMaxDuration() {\n        return meta.getFeature(\"auto-renewal\")\n                .map(Value::asObject)\n                .orElseGet(JSON::empty)\n                .get(\"max-duration\")\n                .asDuration();\n    }\n\n    /**\n     * Returns whether the CA also allows to fetch STAR certificates via GET request.\n     *\n     * @since 2.6\n     * @throws AcmeNotSupportedException if the server does not support auto-renewal.\n     */\n    public boolean isAutoRenewalGetAllowed() {\n        return meta.getFeature(\"auto-renewal\").optional()\n                .map(Value::asObject)\n                .orElseGet(JSON::empty)\n                .get(\"allow-certificate-get\")\n                .optional()\n                .map(Value::asBoolean)\n                .orElse(false);\n    }\n\n    /**\n     * Returns whether the CA supports the profile feature.\n     *\n     * @since 3.5.0\n     * @draft This method is currently based on RFC draft draft-ietf-acme-profiles. It\n     * may be changed or removed without notice to reflect future changes to the draft.\n     * SemVer rules do not apply here.\n     */\n    public boolean isProfileAllowed() {\n        return meta.get(\"profiles\").isPresent();\n    }\n\n    /**\n     * Returns whether the CA supports the requested profile.\n     * <p>\n     * Also returns {@code false} if profiles are not allowed in general.\n     *\n     * @since 3.5.0\n     * @draft This method is currently based on RFC draft draft-ietf-acme-profiles. It\n     * may be changed or removed without notice to reflect future changes to the draft.\n     * SemVer rules do not apply here.\n     */\n    public boolean isProfileAllowed(String profile) {\n        return getProfiles().contains(profile);\n    }\n\n    /**\n     * Returns all profiles supported by the CA. May be empty if the CA does not support\n     * profiles.\n     *\n     * @since 3.5.0\n     * @draft This method is currently based on RFC draft draft-ietf-acme-profiles. It\n     * may be changed or removed without notice to reflect future changes to the draft.\n     * SemVer rules do not apply here.\n     */\n    public Set<String> getProfiles() {\n        return meta.get(\"profiles\")\n                .optional()\n                .map(Value::asObject)\n                .orElseGet(JSON::empty)\n                .keySet();\n    }\n\n    /**\n     * Returns a description of the requested profile. This can be a human-readable string\n     * or a URL linking to a documentation.\n     * <p>\n     * Empty if the profile is not allowed.\n     *\n     * @since 3.5.0\n     * @draft This method is currently based on RFC draft draft-ietf-acme-profiles. It\n     * may be changed or removed without notice to reflect future changes to the draft.\n     * SemVer rules do not apply here.\n     */\n    public Optional<String> getProfileDescription(String profile) {\n        return meta.get(\"profiles\").optional()\n                .map(Value::asObject)\n                .orElseGet(JSON::empty)\n                .get(profile)\n                .optional()\n                .map(Value::asString);\n    }\n\n    /**\n     * Returns whether the CA supports subdomain auth according to RFC9444.\n     *\n     * @since 3.3.0\n     */\n    public boolean isSubdomainAuthAllowed() {\n        return meta.get(\"subdomainAuthAllowed\").map(Value::asBoolean).orElse(false);\n    }\n\n    /**\n     * Returns the JSON representation of the metadata. This is useful for reading\n     * proprietary metadata properties.\n     */\n    public JSON getJSON() {\n        return meta;\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/Order.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static java.util.Collections.unmodifiableList;\nimport static java.util.stream.Collectors.toList;\n\nimport java.io.IOException;\nimport java.io.Serial;\nimport java.net.URL;\nimport java.security.KeyPair;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.EnumSet;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.function.Consumer;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport edu.umd.cs.findbugs.annotations.SuppressFBWarnings;\nimport org.bouncycastle.pkcs.PKCS10CertificationRequest;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeNotSupportedException;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSON.Value;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.util.CSRBuilder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * A representation of a certificate order at the CA.\n */\npublic class Order extends AcmeJsonResource implements PollableResource {\n    @Serial\n    private static final long serialVersionUID = 5435808648658292177L;\n    private static final Logger LOG = LoggerFactory.getLogger(Order.class);\n\n    private transient @Nullable Certificate certificate = null;\n    private transient @Nullable List<Authorization> authorizations = null;\n\n    protected Order(Login login, URL location) {\n        super(login, location);\n    }\n\n    /**\n     * Returns the current status of the order.\n     * <p>\n     * Possible values are: {@link Status#PENDING}, {@link Status#READY},\n     * {@link Status#PROCESSING}, {@link Status#VALID}, {@link Status#INVALID}.\n     * If the server supports STAR, another possible value is {@link Status#CANCELED}.\n     */\n    @Override\n    public Status getStatus() {\n        return getJSON().get(\"status\").asStatus();\n    }\n\n    /**\n     * Returns a {@link Problem} document with the reason if the order has failed.\n     */\n    public Optional<Problem> getError() {\n        return getJSON().get(\"error\").map(v -> v.asProblem(getLocation()));\n    }\n\n    /**\n     * Gets the expiry date of the authorization, if set by the server.\n     */\n    public Optional<Instant> getExpires() {\n        return getJSON().get(\"expires\").map(Value::asInstant);\n    }\n\n    /**\n     * Gets a list of {@link Identifier} that are connected to this order.\n     *\n     * @since 2.3\n     */\n    public List<Identifier> getIdentifiers() {\n        return getJSON().get(\"identifiers\")\n                    .asArray()\n                    .stream()\n                    .map(Value::asIdentifier)\n                    .toList();\n    }\n\n    /**\n     * Gets the \"not before\" date that was used for the order.\n     */\n    public Optional<Instant> getNotBefore() {\n        return getJSON().get(\"notBefore\").map(Value::asInstant);\n    }\n\n    /**\n     * Gets the \"not after\" date that was used for the order.\n     */\n    public Optional<Instant> getNotAfter() {\n        return getJSON().get(\"notAfter\").map(Value::asInstant);\n    }\n\n    /**\n     * Gets the {@link Authorization} that are required to fulfil this order, in no\n     * specific order.\n     */\n    public List<Authorization> getAuthorizations() {\n        if (authorizations == null) {\n            var login = getLogin();\n            authorizations = getJSON().get(\"authorizations\")\n                    .asArray()\n                    .stream()\n                    .map(Value::asURL)\n                    .map(login::bindAuthorization)\n                    .collect(toList());\n        }\n        return unmodifiableList(authorizations);\n    }\n\n    /**\n     * Gets the location {@link URL} of where to send the finalization call to.\n     * <p>\n     * For internal purposes. Use {@link #execute(byte[])} to finalize an order.\n     */\n    public URL getFinalizeLocation() {\n        return getJSON().get(\"finalize\").asURL();\n    }\n\n    /**\n     * Gets the {@link Certificate}.\n     *\n     * @throws IllegalStateException\n     *         if the order is not ready yet. You must finalize the order first, and wait\n     *         for the status to become {@link Status#VALID}.\n     */\n    @SuppressFBWarnings(\"EI_EXPOSE_REP\")    // behavior is intended\n    public Certificate getCertificate() {\n        if (certificate == null) {\n            certificate = getJSON().get(\"star-certificate\")\n                    .optional()\n                    .or(() -> getJSON().get(\"certificate\").optional())\n                    .map(Value::asURL)\n                    .map(getLogin()::bindCertificate)\n                    .orElseThrow(() -> new IllegalStateException(\"Order is not completed\"));\n        }\n        return certificate;\n    }\n\n    /**\n     * Returns whether this is a STAR certificate ({@code true}) or a standard certificate\n     * ({@code false}).\n     *\n     * @since 3.5.0\n     */\n    public boolean isAutoRenewalCertificate() {\n        return getJSON().contains(\"star-certificate\");\n    }\n\n    /**\n     * Finalizes the order.\n     * <p>\n     * If the finalization was successful, the certificate is provided via\n     * {@link #getCertificate()}.\n     * <p>\n     * Even though the ACME protocol uses the term \"finalize an order\", this method is\n     * called {@link #execute(KeyPair)} to avoid confusion with the problematic\n     * {@link Object#finalize()} method.\n     *\n     * @param domainKeyPair\n     *         The {@link KeyPair} that is going to be certified. This is <em>not</em>\n     *         your account's keypair!\n     * @see #execute(KeyPair, Consumer)\n     * @see #execute(PKCS10CertificationRequest)\n     * @see #execute(byte[])\n     * @see #waitUntilReady(Duration)\n     * @see #waitForCompletion(Duration)\n     * @since 3.0.0\n     */\n    public void execute(KeyPair domainKeyPair) throws AcmeException {\n        execute(domainKeyPair, csrBuilder -> {});\n    }\n\n    /**\n     * Finalizes the order (see {@link #execute(KeyPair)}).\n     * <p>\n     * This method also accepts a builderConsumer that can be used to add further details\n     * to the CSR (e.g. your organization). The identifiers (IPs, domain names, etc.) are\n     * automatically added to the CSR.\n     *\n     * @param domainKeyPair\n     *         The {@link KeyPair} that is going to be used together with the certificate.\n     *         This is not your account's keypair!\n     * @param builderConsumer\n     *         {@link Consumer} that adds further details to the provided\n     *         {@link CSRBuilder}.\n     * @see #execute(KeyPair)\n     * @see #execute(PKCS10CertificationRequest)\n     * @see #execute(byte[])\n     * @see #waitUntilReady(Duration)\n     * @see #waitForCompletion(Duration)\n     * @since 3.0.0\n     */\n    public void execute(KeyPair domainKeyPair, Consumer<CSRBuilder> builderConsumer) throws AcmeException {\n        try {\n            var csrBuilder = new CSRBuilder();\n            csrBuilder.addIdentifiers(getIdentifiers());\n            builderConsumer.accept(csrBuilder);\n            csrBuilder.sign(domainKeyPair);\n            execute(csrBuilder.getCSR());\n        } catch (IOException ex) {\n            throw new AcmeException(\"Failed to create CSR\", ex);\n        }\n    }\n\n    /**\n     * Finalizes the order (see {@link #execute(KeyPair)}).\n     * <p>\n     * This method receives a {@link PKCS10CertificationRequest} instance of a CSR that\n     * was generated externally. Use this method to gain full control over the content of\n     * the CSR. The CSR is not checked by acme4j, but just transported to the CA. It is\n     * your responsibility that it matches to the order.\n     *\n     * @param csr\n     *         {@link PKCS10CertificationRequest} to be used for this order.\n     * @see #execute(KeyPair)\n     * @see #execute(KeyPair, Consumer)\n     * @see #execute(byte[])\n     * @see #waitUntilReady(Duration)\n     * @see #waitForCompletion(Duration)\n     * @since 3.0.0\n     */\n    public void execute(PKCS10CertificationRequest csr) throws AcmeException {\n        try {\n            execute(csr.getEncoded());\n        } catch (IOException ex) {\n            throw new AcmeException(\"Invalid CSR\", ex);\n        }\n    }\n\n    /**\n     * Finalizes the order (see {@link #execute(KeyPair)}).\n     * <p>\n     * This method receives a byte array containing an encoded CSR that was generated\n     * externally. Use this method to gain full control over the content of the CSR. The\n     * CSR is not checked by acme4j, but just transported to the CA. It is your\n     * responsibility that it matches to the order.\n     *\n     * @param csr\n     *         Binary representation of a CSR containing the parameters for the\n     *         certificate being requested, in DER format\n     * @see #waitUntilReady(Duration)\n     * @see #waitForCompletion(Duration)\n     */\n    public void execute(byte[] csr) throws AcmeException {\n        LOG.debug(\"finalize\");\n        try (var conn = getSession().connect()) {\n            var claims = new JSONBuilder();\n            claims.putBase64(\"csr\", csr);\n\n            conn.sendSignedRequest(getFinalizeLocation(), claims, getLogin());\n        }\n        invalidate();\n    }\n\n    /**\n     * Waits until the order is ready for finalization.\n     * <p>\n     * Is is ready if it reaches {@link Status#READY}. The method will also return if the\n     * order already has another terminal state, which is either {@link Status#VALID} or\n     * {@link Status#INVALID}.\n     * <p>\n     * This method is synchronous and blocks the current thread.\n     *\n     * @param timeout\n     *         Timeout until a terminal status must have been reached\n     * @return Status that was reached\n     * @since 3.4.0\n     */\n    public Status waitUntilReady(Duration timeout)\n            throws AcmeException, InterruptedException {\n        return waitForStatus(EnumSet.of(Status.READY, Status.VALID, Status.INVALID), timeout);\n    }\n\n    /**\n     * Waits until the order finalization is completed.\n     * <p>\n     * Is is completed if it reaches either {@link Status#VALID} or\n     * {@link Status#INVALID}.\n     * <p>\n     * This method is synchronous and blocks the current thread.\n     *\n     * @param timeout\n     *         Timeout until a terminal status must have been reached\n     * @return Status that was reached\n     * @since 3.4.0\n     */\n    public Status waitForCompletion(Duration timeout)\n            throws AcmeException, InterruptedException {\n        return waitForStatus(EnumSet.of(Status.VALID, Status.INVALID), timeout);\n    }\n\n    /**\n     * Checks if this order is auto-renewing, according to the ACME STAR specifications.\n     *\n     * @since 2.3\n     */\n    public boolean isAutoRenewing() {\n        return getJSON().get(\"auto-renewal\")\n                    .optional()\n                    .isPresent();\n    }\n\n    /**\n     * Returns the earliest date of validity of the first certificate issued.\n     *\n     * @since 2.3\n     * @throws AcmeNotSupportedException if auto-renewal is not supported\n     */\n    public Optional<Instant> getAutoRenewalStartDate() {\n        return getJSON().getFeature(\"auto-renewal\")\n                    .map(Value::asObject)\n                    .orElseGet(JSON::empty)\n                    .get(\"start-date\")\n                    .optional()\n                    .map(Value::asInstant);\n    }\n\n    /**\n     * Returns the latest date of validity of the last certificate issued.\n     *\n     * @since 2.3\n     * @throws AcmeNotSupportedException if auto-renewal is not supported\n     */\n    public Instant getAutoRenewalEndDate() {\n        return getJSON().getFeature(\"auto-renewal\")\n                    .map(Value::asObject)\n                    .orElseGet(JSON::empty)\n                    .get(\"end-date\")\n                    .asInstant();\n    }\n\n    /**\n     * Returns the maximum lifetime of each certificate.\n     *\n     * @since 2.3\n     * @throws AcmeNotSupportedException if auto-renewal is not supported\n     */\n    public Duration getAutoRenewalLifetime() {\n        return getJSON().getFeature(\"auto-renewal\")\n                    .optional()\n                    .map(Value::asObject)\n                    .orElseGet(JSON::empty)\n                    .get(\"lifetime\")\n                    .asDuration();\n    }\n\n    /**\n     * Returns the pre-date period of each certificate.\n     *\n     * @since 2.7\n     * @throws AcmeNotSupportedException if auto-renewal is not supported\n     */\n    public Optional<Duration> getAutoRenewalLifetimeAdjust() {\n        return getJSON().getFeature(\"auto-renewal\")\n                    .optional()\n                    .map(Value::asObject)\n                    .orElseGet(JSON::empty)\n                    .get(\"lifetime-adjust\")\n                    .optional()\n                    .map(Value::asDuration);\n    }\n\n    /**\n     * Returns {@code true} if STAR certificates from this order can also be fetched via\n     * GET requests.\n     *\n     * @since 2.6\n     */\n    public boolean isAutoRenewalGetEnabled() {\n        return getJSON().getFeature(\"auto-renewal\")\n                    .optional()\n                    .map(Value::asObject)\n                    .orElseGet(JSON::empty)\n                    .get(\"allow-certificate-get\")\n                    .optional()\n                    .map(Value::asBoolean)\n                    .orElse(false);\n    }\n\n    /**\n     * Cancels an auto-renewing order.\n     *\n     * @since 2.3\n     */\n    public void cancelAutoRenewal() throws AcmeException {\n        if (!getSession().getMetadata().isAutoRenewalEnabled()) {\n            throw new AcmeNotSupportedException(\"auto-renewal\");\n        }\n\n        LOG.debug(\"cancel\");\n        try (var conn = getSession().connect()) {\n            var claims = new JSONBuilder();\n            claims.put(\"status\", \"canceled\");\n\n            conn.sendSignedRequest(getLocation(), claims, getLogin());\n            setJSON(conn.readJsonResponse());\n        }\n    }\n\n    /**\n     * Returns the selected profile.\n     *\n     * @since 3.5.0\n     * @throws AcmeNotSupportedException if profile is not supported\n     */\n    public String getProfile() {\n        return getJSON().getFeature(\"profile\").asString();\n    }\n\n    @Override\n    protected void invalidate() {\n        super.invalidate();\n        certificate = null;\n        authorizations = null;\n    }\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static java.util.Objects.requireNonNull;\nimport static java.util.stream.Collectors.toList;\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.getRenewalUniqueIdentifier;\n\nimport java.security.cert.X509Certificate;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Collection;\nimport java.util.LinkedHashSet;\nimport java.util.Objects;\nimport java.util.Set;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeNotSupportedException;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Start a new certificate {@link Order}.\n * <p>\n * Use {@link Login#newOrder()} or {@link Account#newOrder()} to create a new\n * {@link OrderBuilder} instance. Both methods are identical.\n */\npublic class OrderBuilder {\n    private static final Logger LOG = LoggerFactory.getLogger(OrderBuilder.class);\n\n    private final Login login;\n\n    private final Set<Identifier> identifierSet = new LinkedHashSet<>();\n    private @Nullable Instant notBefore;\n    private @Nullable Instant notAfter;\n    private @Nullable String replaces;\n    private boolean autoRenewal;\n    private @Nullable Instant autoRenewalStart;\n    private @Nullable Instant autoRenewalEnd;\n    private @Nullable Duration autoRenewalLifetime;\n    private @Nullable Duration autoRenewalLifetimeAdjust;\n    private boolean autoRenewalGet;\n    private @Nullable String profile;\n\n    /**\n     * Create a new {@link OrderBuilder}.\n     *\n     * @param login\n     *            {@link Login} to bind with\n     */\n    protected OrderBuilder(Login login) {\n        this.login = login;\n    }\n\n    /**\n     * Adds a domain name to the order.\n     *\n     * @param domain\n     *            Name of a domain to be ordered. May be a wildcard domain if supported by\n     *            the CA. IDN names are accepted and will be ACE encoded automatically.\n     * @return itself\n     */\n    public OrderBuilder domain(String domain) {\n        return identifier(Identifier.dns(domain));\n    }\n\n    /**\n     * Adds domain names to the order.\n     *\n     * @param domains\n     *            Collection of domain names to be ordered. May be wildcard domains if\n     *            supported by the CA. IDN names are accepted and will be ACE encoded\n     *            automatically.\n     * @return itself\n     */\n    public OrderBuilder domains(String... domains) {\n        for (var domain : requireNonNull(domains, \"domains\")) {\n            domain(domain);\n        }\n        return this;\n    }\n\n    /**\n     * Adds a collection of domain names to the order.\n     *\n     * @param domains\n     *            Collection of domain names to be ordered. May be wildcard domains if\n     *            supported by the CA. IDN names are accepted and will be ACE encoded\n     *            automatically.\n     * @return itself\n     */\n    public OrderBuilder domains(Collection<String> domains) {\n        requireNonNull(domains, \"domains\").forEach(this::domain);\n        return this;\n    }\n\n    /**\n     * Adds an {@link Identifier} to the order.\n     *\n     * @param identifier\n     *            {@link Identifier} to be added to the order.\n     * @return itself\n     * @since 2.3\n     */\n    public OrderBuilder identifier(Identifier identifier) {\n        identifierSet.add(requireNonNull(identifier, \"identifier\"));\n        return this;\n    }\n\n    /**\n     * Adds a collection of {@link Identifier} to the order.\n     *\n     * @param identifiers\n     *            Collection of {@link Identifier} to be added to the order.\n     * @return itself\n     * @since 2.3\n     */\n    public OrderBuilder identifiers(Collection<Identifier> identifiers) {\n        requireNonNull(identifiers, \"identifiers\").forEach(this::identifier);\n        return this;\n    }\n\n    /**\n     * Sets a \"not before\" date in the certificate. May be ignored by the CA.\n     *\n     * @param notBefore \"not before\" date\n     * @return itself\n     */\n    public OrderBuilder notBefore(Instant notBefore) {\n        if (autoRenewal) {\n            throw new IllegalArgumentException(\"cannot combine notBefore with autoRenew\");\n        }\n        this.notBefore = requireNonNull(notBefore, \"notBefore\");\n        return this;\n    }\n\n    /**\n     * Sets a \"not after\" date in the certificate. May be ignored by the CA.\n     *\n     * @param notAfter \"not after\" date\n     * @return itself\n     */\n    public OrderBuilder notAfter(Instant notAfter) {\n        if (autoRenewal) {\n            throw new IllegalArgumentException(\"cannot combine notAfter with autoRenew\");\n        }\n        this.notAfter = requireNonNull(notAfter, \"notAfter\");\n        return this;\n    }\n\n    /**\n     * Enables short-term automatic renewal of the certificate, if supported by the CA.\n     * <p>\n     * Automatic renewals cannot be combined with {@link #notBefore(Instant)} or\n     * {@link #notAfter(Instant)}.\n     *\n     * @return itself\n     * @since 2.3\n     */\n    public OrderBuilder autoRenewal() {\n        if (notBefore != null || notAfter != null) {\n            throw new IllegalArgumentException(\"cannot combine notBefore/notAfter with autoRenewal\");\n        }\n        this.autoRenewal = true;\n        return this;\n    }\n\n    /**\n     * Sets the earliest date of validity of the first issued certificate. If not set,\n     * the start date is the earliest possible date.\n     * <p>\n     * Implies {@link #autoRenewal()}.\n     *\n     * @param start\n     *            Start date of validity\n     * @return itself\n     * @since 2.3\n     */\n    public OrderBuilder autoRenewalStart(Instant start) {\n        autoRenewal();\n        this.autoRenewalStart = requireNonNull(start, \"start\");\n        return this;\n    }\n\n    /**\n     * Sets the latest date of validity of the last issued certificate. If not set, the\n     * CA's default is used.\n     * <p>\n     * Implies {@link #autoRenewal()}.\n     *\n     * @param end\n     *            End date of validity\n     * @return itself\n     * @see Metadata#getAutoRenewalMaxDuration()\n     * @since 2.3\n     */\n    public OrderBuilder autoRenewalEnd(Instant end) {\n        autoRenewal();\n        this.autoRenewalEnd = requireNonNull(end, \"end\");\n        return this;\n    }\n\n    /**\n     * Sets the maximum validity period of each certificate. If not set, the CA's\n     * default is used.\n     * <p>\n     * Implies {@link #autoRenewal()}.\n     *\n     * @param duration\n     *            Duration of validity of each certificate\n     * @return itself\n     * @see Metadata#getAutoRenewalMinLifetime()\n     * @since 2.3\n     */\n    public OrderBuilder autoRenewalLifetime(Duration duration) {\n        autoRenewal();\n        this.autoRenewalLifetime = requireNonNull(duration, \"duration\");\n        return this;\n    }\n\n    /**\n     * Sets the amount of pre-dating each certificate. If not set, the CA's\n     * default (0) is used.\n     * <p>\n     * Implies {@link #autoRenewal()}.\n     *\n     * @param duration\n     *            Duration of certificate pre-dating\n     * @return itself\n     * @since 2.7\n     */\n    public OrderBuilder autoRenewalLifetimeAdjust(Duration duration) {\n        autoRenewal();\n        this.autoRenewalLifetimeAdjust = requireNonNull(duration, \"duration\");\n        return this;\n    }\n\n    /**\n     * Announces that the client wishes to fetch the auto-renewed certificate via GET\n     * request. If not used, the STAR certificate can only be fetched via POST-as-GET\n     * request. {@link Metadata#isAutoRenewalGetAllowed()} must return {@code true} in\n     * order for this option to work.\n     * <p>\n     * This option is only needed if you plan to fetch the STAR certificate via other\n     * means than by using acme4j. acme4j is fetching certificates via POST-as-GET\n     * request.\n     * <p>\n     * Implies {@link #autoRenewal()}.\n     *\n     * @return itself\n     * @since 2.6\n     */\n    public OrderBuilder autoRenewalEnableGet() {\n        autoRenewal();\n        this.autoRenewalGet = true;\n        return this;\n    }\n\n    /**\n     * Notifies the CA of the desired profile of the ordered certificate.\n     * <p>\n     * Optional, only supported if the CA supports profiles. However, in this case the\n     * client <em>may</em> include this field.\n     *\n     * @param profile\n     *         Identifier of the desired profile\n     * @return itself\n     * @draft This method is currently based on RFC draft draft-ietf-acme-profiles. It\n     * may be changed or removed without notice to reflect future changes to the draft.\n     * SemVer rules do not apply here.\n     * @since 3.5.0\n     */\n    public OrderBuilder profile(String profile) {\n        this.profile = Objects.requireNonNull(profile);\n        return this;\n    }\n\n    /**\n     * Notifies the CA that the ordered certificate will replace a previously issued\n     * certificate. The certificate is identified by its ARI unique identifier.\n     * <p>\n     * Optional, only supported if the CA provides renewal information. However, in this\n     * case the client <em>should</em> include this field.\n     *\n     * @param uniqueId\n     *         Certificate's renewal unique identifier.\n     * @return itself\n     * @since 3.2.0\n     */\n    public OrderBuilder replaces(String uniqueId) {\n        this.replaces = Objects.requireNonNull(uniqueId);\n        return this;\n    }\n\n    /**\n     * Notifies the CA that the ordered certificate will replace a previously issued\n     * certificate.\n     * <p>\n     * Optional, only supported if the CA provides renewal information. However, in this\n     * case the client <em>should</em> include this field.\n     *\n     * @param certificate\n     *         Certificate to be replaced\n     * @return itself\n     * @since 3.2.0\n     */\n    public OrderBuilder replaces(X509Certificate certificate) {\n        return replaces(getRenewalUniqueIdentifier(certificate));\n    }\n\n    /**\n     * Notifies the CA that the ordered certificate will replace a previously issued\n     * certificate.\n     * <p>\n     * Optional, only supported if the CA provides renewal information. However, in this\n     * case the client <em>should</em> include this field.\n     *\n     * @param certificate\n     *         Certificate to be replaced\n     * @return itself\n     * @since 3.2.0\n     */\n    public OrderBuilder replaces(Certificate certificate) {\n        return replaces(certificate.getCertificate());\n    }\n\n    /**\n     * Sends a new order to the server, and returns an {@link Order} object.\n     *\n     * @return {@link Order} that was created\n     */\n    public Order create() throws AcmeException {\n        if (identifierSet.isEmpty()) {\n            throw new IllegalArgumentException(\"At least one identifer is required\");\n        }\n\n        var session = login.getSession();\n\n        if (autoRenewal && !session.getMetadata().isAutoRenewalEnabled()) {\n            throw new AcmeNotSupportedException(\"auto-renewal\");\n        }\n\n        if (autoRenewalGet && !session.getMetadata().isAutoRenewalGetAllowed()) {\n            throw new AcmeNotSupportedException(\"auto-renewal-get\");\n        }\n\n        if (replaces != null && session.resourceUrlOptional(Resource.RENEWAL_INFO).isEmpty()) {\n            throw new AcmeNotSupportedException(\"renewal-information\");\n        }\n\n        if (profile != null && !session.getMetadata().isProfileAllowed()) {\n            throw new AcmeNotSupportedException(\"profile\");\n        }\n\n        if (profile != null && !session.getMetadata().isProfileAllowed(profile)) {\n            throw new AcmeNotSupportedException(\"profile: \" + profile);\n        }\n\n        var hasAncestorDomain = identifierSet.stream()\n                .filter(id -> Identifier.TYPE_DNS.equals(id.getType()))\n                .anyMatch(id -> id.toMap().containsKey(Identifier.KEY_ANCESTOR_DOMAIN));\n        if (hasAncestorDomain && !login.getSession().getMetadata().isSubdomainAuthAllowed()) {\n            throw new AcmeNotSupportedException(\"ancestor-domain\");\n        }\n\n        LOG.debug(\"create\");\n        try (var conn = session.connect()) {\n            var claims = new JSONBuilder();\n            claims.array(\"identifiers\", identifierSet.stream().map(Identifier::toMap).collect(toList()));\n\n            if (notBefore != null) {\n                claims.put(\"notBefore\", notBefore);\n            }\n            if (notAfter != null) {\n                claims.put(\"notAfter\", notAfter);\n            }\n\n            if (autoRenewal) {\n                var arClaims = claims.object(\"auto-renewal\");\n                if (autoRenewalStart != null) {\n                    arClaims.put(\"start-date\", autoRenewalStart);\n                }\n                if (autoRenewalStart != null) {\n                    arClaims.put(\"end-date\", autoRenewalEnd);\n                }\n                if (autoRenewalLifetime != null) {\n                    arClaims.put(\"lifetime\", autoRenewalLifetime);\n                }\n                if (autoRenewalLifetimeAdjust != null) {\n                    arClaims.put(\"lifetime-adjust\", autoRenewalLifetimeAdjust);\n                }\n                if (autoRenewalGet) {\n                    arClaims.put(\"allow-certificate-get\", autoRenewalGet);\n                }\n            }\n\n            if (replaces != null) {\n                claims.put(\"replaces\", replaces);\n            }\n\n            if (profile != null) {\n                claims.put(\"profile\", profile);\n            }\n\n            conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login);\n\n            var order = new Order(login, conn.getLocation());\n            order.setJSON(conn.readJsonResponse());\n            return order;\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/PollableResource.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2024 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static java.time.Instant.now;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\n\nimport org.shredzone.acme4j.exception.AcmeException;\n\n/**\n * Marks an ACME Resource with a pollable status.\n * <p>\n * The resource provides a status, and a method for updating the internal cache to read\n * the current status from the server.\n *\n * @since 3.4.0\n */\npublic interface PollableResource {\n\n    /**\n     * Default delay between status polls if there is no Retry-After header.\n     */\n    Duration DEFAULT_RETRY_AFTER = Duration.ofSeconds(3L);\n\n    /**\n     * Returns the current status of the resource.\n     */\n    Status getStatus();\n\n    /**\n     * Fetches the current status from the server.\n     *\n     * @return Retry-After time, if given by the CA, otherwise empty.\n     */\n    Optional<Instant> fetch() throws AcmeException;\n\n    /**\n     * Waits until a terminal status has been reached, by polling until one of the given\n     * status or the given timeout has been reached. This call honors the Retry-After\n     * header if set by the CA.\n     * <p>\n     * This method is synchronous and blocks the current thread.\n     * <p>\n     * If the resource is already in a terminal status, the method returns immediately.\n     *\n     * @param statusSet\n     *         Set of {@link Status} that are accepted as terminal\n     * @param timeout\n     *         Timeout until a terminal status must have been reached\n     * @return Status that was reached\n     */\n    default Status waitForStatus(Set<Status> statusSet, Duration timeout)\n            throws AcmeException, InterruptedException {\n        Objects.requireNonNull(timeout, \"timeout\");\n        Objects.requireNonNull(statusSet, \"statusSet\");\n        if (statusSet.isEmpty()) {\n            throw new IllegalArgumentException(\"At least one Status is required\");\n        }\n\n        var currentStatus = getStatus();\n        if (statusSet.contains(currentStatus)) {\n            return currentStatus;\n        }\n\n        var timebox = now().plus(timeout);\n        Instant now;\n\n        while ((now = now()).isBefore(timebox)) {\n            // Poll status and get the time of the next poll\n            var retryAfter = fetch()\n                    .orElse(now.plus(DEFAULT_RETRY_AFTER));\n\n            currentStatus = getStatus();\n            if (statusSet.contains(currentStatus)) {\n                return currentStatus;\n            }\n\n            // Preemptively end the loop if the next iteration would be after timebox\n            if (retryAfter.isAfter(timebox)) {\n                break;\n            }\n\n            // Wait until retryAfter is reached\n            Thread.sleep(now.until(retryAfter, ChronoUnit.MILLIS));\n        }\n\n        throw new AcmeException(\"Timeout has been reached\");\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/Problem.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.util.List;\nimport java.util.Optional;\n\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSON.Value;\n\n/**\n * A JSON problem. It contains further, machine- and human-readable details about the\n * reason of an error or failure.\n *\n * @see <a href=\"https://tools.ietf.org/html/rfc7807\">RFC 7807</a>\n */\npublic class Problem implements Serializable {\n    @Serial\n    private static final long serialVersionUID = -8418248862966754214L;\n\n    private final URL baseUrl;\n    private final JSON problemJson;\n\n    /**\n     * Creates a new {@link Problem} object.\n     *\n     * @param problem\n     *            Problem as JSON structure\n     * @param baseUrl\n     *            Document's base {@link URL} to resolve relative URIs against\n     */\n    public Problem(JSON problem, URL baseUrl) {\n        this.problemJson = problem;\n        this.baseUrl = baseUrl;\n    }\n\n    /**\n     * Returns the problem type. It is always an absolute URI.\n     */\n    public URI getType() {\n        return problemJson.get(\"type\")\n                    .map(Value::asString)\n                    .map(it -> {\n                        try {\n                            return baseUrl.toURI().resolve(it);\n                        } catch (URISyntaxException ex) {\n                            throw new IllegalArgumentException(\"Bad base URL\", ex);\n                        }\n                    })\n                    .orElseThrow(() -> new AcmeProtocolException(\"Problem without type\"));\n    }\n\n    /**\n     * Returns a short, human-readable summary of the problem. The text may be localized\n     * if supported by the server. Empty if the server did not provide a title.\n     *\n     * @see #toString()\n     */\n    public Optional<String> getTitle() {\n        return problemJson.get(\"title\").map(Value::asString);\n    }\n\n    /**\n     * Returns a detailed and specific human-readable explanation of the problem. The\n     * text may be localized if supported by the server.\n     *\n     * @see #toString()\n     */\n    public Optional<String> getDetail() {\n        return problemJson.get(\"detail\").map(Value::asString);\n    }\n\n    /**\n     * Returns a URI that identifies the specific occurence of the problem. It is always\n     * an absolute URI.\n     */\n    public Optional<URI> getInstance() {\n        return problemJson.get(\"instance\")\n                        .map(Value::asString)\n                        .map(it ->  {\n                            try {\n                                return baseUrl.toURI().resolve(it);\n                            } catch (URISyntaxException ex) {\n                                throw new IllegalArgumentException(\"Bad base URL\", ex);\n                            }\n                        });\n    }\n\n    /**\n     * Returns the {@link Identifier} this problem relates to.\n     *\n     * @since 2.3\n     */\n    public Optional<Identifier> getIdentifier() {\n        return problemJson.get(\"identifier\")\n                        .optional()\n                        .map(Value::asIdentifier);\n    }\n\n    /**\n     * Returns a list of sub-problems.\n     */\n    public List<Problem> getSubProblems() {\n        return problemJson.get(\"subproblems\")\n                        .asArray()\n                        .stream()\n                        .map(o -> o.asProblem(baseUrl))\n                        .toList();\n    }\n\n    /**\n     * Returns the problem as {@link JSON} object, to access other, non-standard fields.\n     *\n     * @return Problem as {@link JSON} object\n     */\n    public JSON asJSON() {\n        return problemJson;\n    }\n\n    /**\n     * Returns a human-readable description of the problem, that is as specific as\n     * possible. The description may be localized if supported by the server.\n     * <p>\n     * If {@link #getSubProblems()} exist, they will be appended.\n     * <p>\n     * Technically, it returns {@link #getDetail()}. If not set, {@link #getTitle()} is\n     * returned instead. As a last resort, {@link #getType()} is returned.\n     */\n    @Override\n    public String toString() {\n        var sb = new StringBuilder();\n\n        if (getDetail().isPresent()) {\n            sb.append(getDetail().get());\n        } else if (getTitle().isPresent()) {\n            sb.append(getTitle().get());\n        } else {\n            sb.append(getType());\n        }\n\n        var subproblems = getSubProblems();\n\n        if (!subproblems.isEmpty()) {\n            sb.append(\" (\");\n            var first = true;\n            for (var sub : subproblems) {\n                if (!first) {\n                    sb.append(\" ‒ \");\n                }\n                sb.append(sub.toString());\n                first = false;\n            }\n            sb.append(')');\n        }\n\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/RenewalInfo.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2023 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport java.net.URL;\nimport java.time.Instant;\nimport java.time.temporal.TemporalAmount;\nimport java.util.Optional;\nimport java.util.concurrent.ThreadLocalRandom;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.acme4j.connector.Connection;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSON.Value;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Renewal Information of a certificate.\n *\n * @since 3.0.0\n */\npublic class RenewalInfo extends AcmeJsonResource {\n    private static final Logger LOG = LoggerFactory.getLogger(RenewalInfo.class);\n\n    protected RenewalInfo(Login login, URL location) {\n        super(login, location);\n    }\n\n    /**\n     * Returns the starting {@link Instant} of the time window the CA recommends for\n     * certificate renewal.\n     */\n    public Instant getSuggestedWindowStart() {\n        return getJSON().get(\"suggestedWindow\").asObject().get(\"start\").asInstant();\n    }\n\n    /**\n     * Returns the ending {@link Instant} of the time window the CA recommends for\n     * certificate renewal.\n     */\n    public Instant getSuggestedWindowEnd() {\n        return getJSON().get(\"suggestedWindow\").asObject().get(\"end\").asInstant();\n    }\n\n    /**\n     * An optional {@link URL} pointing to a page which may explain why the suggested\n     * renewal window is what it is.\n     */\n    public Optional<URL> getExplanation() {\n        return getJSON().get(\"explanationURL\").optional().map(Value::asURL);\n    }\n\n    /**\n     * Checks if the given {@link Instant} is before the suggested time window, so a\n     * certificate renewal is not required yet.\n     *\n     * @param instant\n     *         {@link Instant} to check\n     * @return {@code true} if the {@link Instant} is before the time window, {@code\n     * false} otherwise.\n     */\n    public boolean renewalIsNotRequired(Instant instant) {\n        assertValidTimeWindow();\n        return instant.isBefore(getSuggestedWindowStart());\n    }\n\n    /**\n     * Checks if the given {@link Instant} is within the suggested time window, and a\n     * certificate renewal is recommended.\n     * <p>\n     * An {@link Instant} is deemed to be within the time window if it is equal to, or\n     * after {@link #getSuggestedWindowStart()}, and before {@link\n     * #getSuggestedWindowEnd()}.\n     *\n     * @param instant\n     *         {@link Instant} to check\n     * @return {@code true} if the {@link Instant} is within the time window, {@code\n     * false} otherwise.\n     */\n    public boolean renewalIsRecommended(Instant instant) {\n        assertValidTimeWindow();\n        return !instant.isBefore(getSuggestedWindowStart())\n                && instant.isBefore(getSuggestedWindowEnd());\n    }\n\n    /**\n     * Checks if the given {@link Instant} is past the time window, and a certificate\n     * renewal is overdue.\n     * <p>\n     * An {@link Instant} is deemed to be past the time window if it is equal to, or after\n     * {@link #getSuggestedWindowEnd()}.\n     *\n     * @param instant\n     *         {@link Instant} to check\n     * @return {@code true} if the {@link Instant} is past the time window, {@code false}\n     * otherwise.\n     */\n    public boolean renewalIsOverdue(Instant instant) {\n        assertValidTimeWindow();\n        return !instant.isBefore(getSuggestedWindowEnd());\n    }\n\n    /**\n     * Returns a proposed {@link Instant} when the certificate related to this\n     * {@link RenewalInfo} should be renewed.\n     * <p>\n     * This method is useful for setting alarms for renewal cron jobs. As a parameter, the\n     * frequency of the cron job is set. The resulting {@link Instant} is guaranteed to be\n     * executed in time, considering the cron job intervals.\n     * <p>\n     * This method uses {@link ThreadLocalRandom} for random numbers. It is sufficient for\n     * most cases, as only an \"earliest\" {@link Instant} is returned, but the actual\n     * renewal process also depends on cron job execution times and other factors like\n     * system load.\n     * <p>\n     * The result is empty if it is impossible to renew the certificate in time, under the\n     * given circumstances. This is either because the time window already ended in the\n     * past, or because the cron job would not be executed before the ending of the time\n     * window. In this case, it is recommended to renew the certificate immediately.\n     *\n     * @param frequency\n     *         Frequency of the cron job executing the certificate renewals. May be\n     *         {@code null} if there is no cron job, and the renewal is going to be\n     *         executed exactly at the given {@link Instant}.\n     * @return Random {@link Instant} when the certificate should be renewed. This instant\n     * might be slightly in the past. In this case, start the renewal process at the next\n     * possible regular moment.\n     */\n    public Optional<Instant> getRandomProposal(@Nullable TemporalAmount frequency) {\n        assertValidTimeWindow();\n        Instant start = Instant.now();\n        Instant suggestedStart = getSuggestedWindowStart();\n        if (start.isBefore(suggestedStart)) {\n            start = suggestedStart;\n        }\n\n        Instant end = getSuggestedWindowEnd();\n        if (frequency != null) {\n            end = end.minus(frequency);\n        }\n\n        if (!end.isAfter(start)) {\n            return Optional.empty();\n        }\n\n        return Optional.of(Instant.ofEpochMilli(ThreadLocalRandom.current().nextLong(\n                start.toEpochMilli(),\n                end.toEpochMilli())));\n    }\n\n    /**\n     * Asserts that the end of the suggested time window is after the start.\n     */\n    private void assertValidTimeWindow() {\n        if (getSuggestedWindowStart().isAfter(getSuggestedWindowEnd())) {\n            throw new AcmeProtocolException(\"Received an invalid suggested window\");\n        }\n    }\n\n    @Override\n    public Optional<Instant> fetch() throws AcmeException {\n        LOG.debug(\"update RenewalInfo\");\n        try (Connection conn = getSession().connect()) {\n            conn.sendRequest(getLocation(), getSession(), null);\n            setJSON(conn.readJsonResponse());\n            var retryAfterOpt = conn.getRetryAfter();\n            retryAfterOpt.ifPresent(instant -> LOG.debug(\"Retry-After: {}\", instant));\n            setRetryAfter(retryAfterOpt.orElse(null));\n            return retryAfterOpt;\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/RevocationReason.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport java.util.Arrays;\n\n/**\n * An enumeration of revocation reasons.\n *\n * @see <a href=\"https://tools.ietf.org/html/rfc5280#section-5.3.1\">RFC 5280 Section\n * 5.3.1</a>\n */\npublic enum RevocationReason {\n\n    UNSPECIFIED(0),\n    KEY_COMPROMISE(1),\n    CA_COMPROMISE(2),\n    AFFILIATION_CHANGED(3),\n    SUPERSEDED(4),\n    CESSATION_OF_OPERATION(5),\n    CERTIFICATE_HOLD(6),\n    REMOVE_FROM_CRL(8),\n    PRIVILEGE_WITHDRAWN(9),\n    AA_COMPROMISE(10);\n\n    private final int reasonCode;\n\n    RevocationReason(int reasonCode) {\n        this.reasonCode = reasonCode;\n    }\n\n    /**\n     * Returns the reason code as defined in RFC 5280.\n     */\n    public int getReasonCode() {\n        return reasonCode;\n    }\n\n    /**\n     * Returns the {@link RevocationReason} that matches the reason code.\n     *\n     * @param reasonCode\n     *            Reason code as defined in RFC 5280\n     * @return Matching {@link RevocationReason}\n     * @throws IllegalArgumentException if the reason code is unknown or invalid\n     */\n    public static RevocationReason code(int reasonCode) {\n        return Arrays.stream(values())\n                .filter(rr -> rr.reasonCode == reasonCode)\n                .findFirst()\n                .orElseThrow(() -> new IllegalArgumentException(\"Unknown revocation reason code: \" + reasonCode));\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/Session.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static java.util.Objects.requireNonNull;\n\nimport java.net.URI;\nimport java.net.URL;\nimport java.net.http.HttpClient;\nimport java.security.KeyPair;\nimport java.time.ZonedDateTime;\nimport java.util.EnumMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.ServiceLoader;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.stream.StreamSupport;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport edu.umd.cs.findbugs.annotations.SuppressFBWarnings;\nimport org.shredzone.acme4j.connector.Connection;\nimport org.shredzone.acme4j.connector.NetworkSettings;\nimport org.shredzone.acme4j.connector.NonceHolder;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeNotSupportedException;\nimport org.shredzone.acme4j.provider.AcmeProvider;\nimport org.shredzone.acme4j.provider.GenericAcmeProvider;\nimport org.shredzone.acme4j.toolbox.AcmeUtils;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSON.Value;\n\n/**\n * A {@link Session} tracks the entire communication with a CA.\n * <p>\n * To create a session instance, use its constructor. It requires the URI of the ACME\n * server to connect to. This can be the location of the CA's directory (via {@code http}\n * or {@code https} protocol), or a special URI (via {@code acme} protocol). See the\n * documentation about valid URIs.\n * <p>\n * Starting with version 4.0.0, a session instance can be shared between multiple threads.\n * A session won't perform parallel HTTP connections. For high-load scenarios, it is\n * recommended to use multiple sessions.\n */\npublic class Session {\n\n    private static final GenericAcmeProvider GENERIC_PROVIDER = new GenericAcmeProvider();\n\n    private final AtomicReference<Map<Resource, URL>> resourceMap = new AtomicReference<>();\n    private final AtomicReference<Metadata> metadata = new AtomicReference<>();\n    private final AtomicReference<HttpClient> httpClient = new AtomicReference<>();\n    private final ReentrantLock nonceLock = new ReentrantLock();\n    private final NetworkSettings networkSettings = new NetworkSettings();\n    private final URI serverUri;\n    private final AcmeProvider provider;\n\n    private @Nullable String nonce;\n    private @Nullable Locale locale = Locale.getDefault();\n    private String languageHeader = AcmeUtils.localeToLanguageHeader(Locale.getDefault());\n    protected @Nullable ZonedDateTime directoryLastModified;\n    protected @Nullable ZonedDateTime directoryExpires;\n\n    /**\n     * Creates a new {@link Session}.\n     *\n     * @param serverUri\n     *         URI string of the ACME server to connect to. This is either the location of\n     *         the CA's ACME directory (using {@code http} or {@code https} protocol), or\n     *         a special URI (using the {@code acme} protocol).\n     * @throws IllegalArgumentException\n     *         if no ACME provider was found for the server URI.\n     */\n    public Session(String serverUri) {\n        this(URI.create(serverUri));\n    }\n\n    /**\n     * Creates a new {@link Session}.\n     *\n     * @param serverUri\n     *         {@link URI} of the ACME server to connect to. This is either the location\n     *         of the CA's ACME directory (using {@code http} or {@code https} protocol),\n     *         or a special URI (using the {@code acme} protocol).\n     * @throws IllegalArgumentException\n     *         if no ACME provider was found for the server URI.\n     */\n    public Session(URI serverUri) {\n        this.serverUri = requireNonNull(serverUri, \"serverUri\");\n\n        if (GENERIC_PROVIDER.accepts(serverUri)) {\n            provider = GENERIC_PROVIDER;\n            return;\n        }\n\n        var providers = ServiceLoader.load(AcmeProvider.class);\n        provider = StreamSupport.stream(providers.spliterator(), false)\n            .filter(p -> p.accepts(serverUri))\n            .reduce((a, b) -> {\n                    throw new IllegalArgumentException(\"Both ACME providers \"\n                        + a.getClass().getSimpleName() + \" and \"\n                        + b.getClass().getSimpleName() + \" accept \"\n                        + serverUri + \". Please check your classpath.\");\n                })\n            .orElseThrow(() -> new IllegalArgumentException(\"No ACME provider found for \" + serverUri));\n    }\n\n    /**\n     * Creates a new {@link Session} using the given {@link AcmeProvider}.\n     * <p>\n     * This constructor is only to be used for testing purposes.\n     *\n     * @param serverUri\n     *         {@link URI} of the ACME server\n     * @param provider\n     *         {@link AcmeProvider} to be used\n     * @since 2.8\n     */\n    public Session(URI serverUri, AcmeProvider provider) {\n        this.serverUri = requireNonNull(serverUri, \"serverUri\");\n        this.provider = requireNonNull(provider, \"provider\");\n\n        if (!provider.accepts(serverUri)) {\n            throw new IllegalArgumentException(\"Provider does not accept \" + serverUri);\n        }\n    }\n\n    /**\n     * Logs into an existing account.\n     *\n     * @param accountLocation\n     *            Location {@link URL} of the account\n     * @param accountKeyPair\n     *            Account {@link KeyPair}\n     * @return {@link Login} to this account\n     */\n    public Login login(URL accountLocation, KeyPair accountKeyPair) {\n        return new Login(accountLocation, accountKeyPair, this);\n    }\n\n    /**\n     * Gets the ACME server {@link URI} of this session.\n     */\n    public URI getServerUri() {\n        return serverUri;\n    }\n\n    /**\n     * Locks the Session for the current thread, and returns a {@link NonceHolder}.\n     * <p>\n     * The current thread can lock the nonce multiple times. Other threads have to wait\n     * until the current thread unlocks the nonce.\n     *\n     * @since 4.0.0\n     */\n    public NonceHolder lockNonce() {\n        nonceLock.lock();\n        return new NonceHolder() {\n            @Override\n            public String getNonce() {\n                return Session.this.nonce;\n            }\n\n            @Override\n            public void setNonce(@Nullable String nonce) {\n                Session.this.nonce = nonce;\n            }\n\n            @Override\n            public void close() {\n                nonceLock.unlock();\n            }\n        };\n    }\n\n    /**\n     * Gets the current locale of this session, or {@code null} if no special language is\n     * selected.\n     */\n    @Nullable\n    public Locale getLocale() {\n        return locale;\n    }\n\n    /**\n     * Sets the locale used in this session. The locale is passed to the server as\n     * Accept-Language header. The server <em>may</em> respond with localized messages.\n     * The default is the system's language. If set to {@code null}, any language will be\n     * accepted.\n     */\n    public void setLocale(@Nullable Locale locale) {\n        this.locale = locale;\n        this.languageHeader = AcmeUtils.localeToLanguageHeader(locale);\n    }\n\n    /**\n     * Gets an Accept-Language header value that matches the current locale. This method\n     * is mainly for internal use.\n     *\n     * @since 3.0.0\n     */\n    public String getLanguageHeader() {\n        return languageHeader;\n    }\n\n    /**\n     * Returns the current {@link NetworkSettings}.\n     *\n     * @return {@link NetworkSettings}\n     * @since 2.8\n     */\n    @SuppressFBWarnings(\"EI_EXPOSE_REP\")    // behavior is intended\n    public NetworkSettings networkSettings() {\n        return networkSettings;\n    }\n\n    /**\n     * Returns the {@link AcmeProvider} that is used for this session.\n     *\n     * @return {@link AcmeProvider}\n     */\n    public AcmeProvider provider() {\n        return provider;\n    }\n\n    /**\n     * Returns a new {@link Connection} to the ACME server.\n     *\n     * @return {@link Connection}\n     */\n    public Connection connect() {\n        return provider.connect(getServerUri(), networkSettings, getHttpClient());\n    }\n\n    /**\n     * Returns the shared {@link HttpClient} instance for this session. The instance is\n     * created lazily on first access and then cached for reuse. This allows multiple\n     * connections to share the same HTTP client, improving resource utilization and\n     * connection pooling.\n     *\n     * @return Shared {@link HttpClient} instance\n     * @since 4.0.0\n     */\n    public HttpClient getHttpClient() {\n        var result = httpClient.get();\n        if (result == null) {\n            result = httpClient.updateAndGet(\n                    client -> client != null\n                            ? client\n                            : provider.createHttpClient(networkSettings)\n            );\n        }\n        return result;\n    }\n\n    /**\n     * Gets the {@link URL} of the given {@link Resource}. This may involve connecting to\n     * the server and fetching the directory. The result is cached.\n     *\n     * @param resource\n     *            {@link Resource} to get the {@link URL} of\n     * @return {@link URL} of the resource\n     * @throws AcmeException\n     *             if the server does not offer the {@link Resource}\n     */\n    public URL resourceUrl(Resource resource) throws AcmeException {\n        return resourceUrlOptional(resource)\n                .orElseThrow(() -> new AcmeNotSupportedException(resource.path()));\n    }\n\n    /**\n     * Gets the {@link URL} of the given {@link Resource}. This may involve connecting to\n     * the server and fetching the directory. The result is cached.\n     *\n     * @param resource\n     *            {@link Resource} to get the {@link URL} of\n     * @return {@link URL} of the resource, or empty if the resource is not available.\n     * @since 3.0.0\n     */\n    public Optional<URL> resourceUrlOptional(Resource resource) throws AcmeException {\n        readDirectory();\n        return Optional.ofNullable(resourceMap.get()\n                .get(requireNonNull(resource, \"resource\")));\n    }\n\n    /**\n     * Gets the metadata of the provider's directory. This may involve connecting to the\n     * server and fetching the directory. The result is cached.\n     *\n     * @return {@link Metadata}. May contain no data, but is never {@code null}.\n     */\n    public Metadata getMetadata() throws AcmeException {\n        readDirectory();\n        return metadata.get();\n    }\n\n    /**\n     * Returns the date when the directory has been modified the last time.\n     *\n     * @return The last modification date of the directory, or {@code null} if unknown\n     * (directory has not been read yet or did not provide this information).\n     * @since 2.10\n     */\n    @Nullable\n    public ZonedDateTime getDirectoryLastModified() {\n        return directoryLastModified;\n    }\n\n    /**\n     * Sets the date when the directory has been modified the last time. Should only be\n     * invoked by {@link AcmeProvider} implementations.\n     *\n     * @param directoryLastModified\n     *         The last modification date of the directory, or {@code null} if unknown\n     *         (directory has not been read yet or did not provide this information).\n     * @since 2.10\n     */\n    public void setDirectoryLastModified(@Nullable ZonedDateTime directoryLastModified) {\n        this.directoryLastModified = directoryLastModified;\n    }\n\n    /**\n     * Returns the date when the current directory records will expire. A fresh copy of\n     * the directory will be fetched automatically after that instant.\n     *\n     * @return The expiration date, or {@code null} if the server did not provide this\n     * information.\n     * @since 2.10\n     */\n    @Nullable\n    public ZonedDateTime getDirectoryExpires() {\n        return directoryExpires;\n    }\n\n    /**\n     * Sets the date when the current directory will expire. Should only be invoked by\n     * {@link AcmeProvider} implementations.\n     *\n     * @param directoryExpires\n     *         Expiration date, or {@code null} if the server did not provide this\n     *         information.\n     * @since 2.10\n     */\n    public void setDirectoryExpires(@Nullable ZonedDateTime directoryExpires) {\n        this.directoryExpires = directoryExpires;\n    }\n\n    /**\n     * Returns {@code true} if a copy of the directory is present in a local cache. It is\n     * not evaluated if the cached copy has expired though.\n     *\n     * @return {@code true} if a directory is available.\n     * @since 2.10\n     */\n    public boolean hasDirectory() {\n        return resourceMap.get() != null;\n    }\n\n    /**\n     * Purges the directory cache. Makes sure that a fresh copy of the directory will be\n     * read from the CA on the next time the directory is accessed.\n     *\n     * @since 3.0.0\n     */\n    public void purgeDirectoryCache() {\n        setDirectoryLastModified(null);\n        setDirectoryExpires(null);\n        resourceMap.set(null);\n    }\n\n    /**\n     * Reads the provider's directory, then rebuild the resource map. The resource map\n     * is unchanged if the {@link AcmeProvider} returns that the directory has not been\n     * changed on the remote side.\n     */\n    private void readDirectory() throws AcmeException {\n        var directoryJson = provider().directory(this, getServerUri());\n        if (directoryJson == null) {\n            if (!hasDirectory()) {\n                throw new AcmeException(\"AcmeProvider did not provide a directory\");\n            }\n            return;\n        }\n\n        var meta = directoryJson.get(\"meta\");\n        if (meta.isPresent()) {\n            metadata.set(new Metadata(meta.asObject()));\n        } else {\n            metadata.set(new Metadata(JSON.empty()));\n        }\n\n        var map = new EnumMap<Resource, URL>(Resource.class);\n        for (var res : Resource.values()) {\n            directoryJson.get(res.path())\n                    .map(Value::asURL)\n                    .ifPresent(url -> map.put(res, url));\n        }\n\n        resourceMap.set(map);\n    }\n\n    @Override\n    protected final void finalize() {\n        // CT_CONSTRUCTOR_THROW: Prevents finalizer attack\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/Status.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport java.util.Arrays;\nimport java.util.Locale;\n\n/**\n * An enumeration of status codes of challenges and authorizations.\n */\npublic enum Status {\n\n    /**\n     * The server has created the resource, and is waiting for the client to process it.\n     */\n    PENDING,\n\n    /**\n     * The {@link Order} is ready to be finalized. Invoke {@link Order#execute(byte[])}.\n     */\n    READY,\n\n    /**\n     * The server is processing the resource. The client should invoke\n     * {@link AcmeJsonResource#fetch()} and re-check the status.\n     */\n    PROCESSING,\n\n    /**\n     * The resource is valid and can be used as intended.\n     */\n    VALID,\n\n    /**\n     * An error or authorization/validation failure has occured. The client should check\n     * for error messages.\n     */\n    INVALID,\n\n    /**\n     * The {@link Authorization} has been revoked by the server.\n     */\n    REVOKED,\n\n    /**\n     * The {@link Account} or {@link Authorization} has been deactivated by the client.\n     */\n    DEACTIVATED,\n\n    /**\n     * The {@link Authorization} is expired.\n     */\n    EXPIRED,\n\n    /**\n     * An auto-renewing {@link Order} is canceled.\n     *\n     * @since 2.3\n     */\n    CANCELED,\n\n    /**\n     * The server did not provide a status, or the provided status is not a specified ACME\n     * status.\n     */\n    UNKNOWN;\n\n    /**\n     * Parses the string and returns a corresponding Status object.\n     *\n     * @param str\n     *            String to parse\n     * @return {@link Status} matching the string, or {@link Status#UNKNOWN} if there was\n     *         no match\n     */\n    public static Status parse(String str) {\n        var check = str.toUpperCase(Locale.ENGLISH);\n        return Arrays.stream(values())\n                .filter(s -> s.name().equals(check))\n                .findFirst()\n                .orElse(Status.UNKNOWN);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport java.io.Serial;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.EnumSet;\nimport java.util.Optional;\n\nimport org.shredzone.acme4j.AcmeJsonResource;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.PollableResource;\nimport org.shredzone.acme4j.Problem;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSON.Value;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * A generic challenge. It can be used as a base class for actual challenge\n * implementations, but it is also used if the ACME server offers a proprietary challenge\n * that is unknown to acme4j.\n * <p>\n * Subclasses must override {@link Challenge#acceptable(String)} so it only accepts its\n * own type. {@link Challenge#prepareResponse(JSONBuilder)} can be overridden to put all\n * required data to the challenge response.\n */\npublic class Challenge extends AcmeJsonResource implements PollableResource {\n    @Serial\n    private static final long serialVersionUID = 2338794776848388099L;\n    private static final Logger LOG = LoggerFactory.getLogger(Challenge.class);\n\n    protected static final String KEY_TYPE = \"type\";\n    protected static final String KEY_URL = \"url\";\n    protected static final String KEY_STATUS = \"status\";\n    protected static final String KEY_VALIDATED = \"validated\";\n    protected static final String KEY_ERROR = \"error\";\n\n    /**\n     * Creates a new generic {@link Challenge} object.\n     *\n     * @param login\n     *            {@link Login} the resource is bound with\n     * @param data\n     *            {@link JSON} challenge data\n     */\n    public Challenge(Login login, JSON data) {\n        super(login, data.get(KEY_URL).asURL());\n        setJSON(data);\n    }\n\n    /**\n     * Returns the challenge type by name (e.g. \"http-01\").\n     */\n    public String getType() {\n        return getJSON().get(KEY_TYPE).asString();\n    }\n\n    /**\n     * Returns the current status of the challenge.\n     * <p>\n     * Possible values are: {@link Status#PENDING}, {@link Status#PROCESSING},\n     * {@link Status#VALID}, {@link Status#INVALID}.\n     * <p>\n     * A challenge is only completed when it reaches either status {@link Status#VALID} or\n     * {@link Status#INVALID}.\n     */\n    @Override\n    public Status getStatus() {\n        return getJSON().get(KEY_STATUS).asStatus();\n    }\n\n    /**\n     * Returns the validation date, if returned by the server.\n     */\n    public Optional<Instant> getValidated() {\n        return getJSON().get(KEY_VALIDATED).map(Value::asInstant);\n    }\n\n    /**\n     * Returns a reason why the challenge has failed in the past, if returned by the\n     * server. If there are multiple errors, they can be found in\n     * {@link Problem#getSubProblems()}.\n     */\n    public Optional<Problem> getError() {\n        return getJSON().get(KEY_ERROR).map(it -> it.asProblem(getLocation()));\n    }\n\n    /**\n     * Prepares the response message for triggering the challenge. Subclasses can add\n     * fields to the {@link JSONBuilder} as required by the challenge. Implementations of\n     * subclasses should make sure that {@link #prepareResponse(JSONBuilder)} of the\n     * superclass is invoked.\n     *\n     * @param response\n     *         {@link JSONBuilder} to write the response to\n     */\n    protected void prepareResponse(JSONBuilder response) {\n        // Do nothing here...\n    }\n\n    /**\n     * Checks if the type is acceptable to this challenge. This generic class only checks\n     * if the type is not blank. Subclasses should instead check if the given type matches\n     * expected challenge type.\n     *\n     * @param type\n     *         Type to check\n     * @return {@code true} if acceptable, {@code false} if not\n     */\n    protected boolean acceptable(String type) {\n        return type != null && !type.trim().isEmpty();\n    }\n\n    @Override\n    protected void setJSON(JSON json) {\n        var type = json.get(KEY_TYPE).asString();\n\n        if (!acceptable(type)) {\n            throw new AcmeProtocolException(\"incompatible type \" + type + \" for this challenge\");\n        }\n\n        var loc = json.get(KEY_URL).asString();\n        if (!loc.equals(getLocation().toString())) {\n            throw new AcmeProtocolException(\"challenge has changed its location\");\n        }\n\n        super.setJSON(json);\n    }\n\n    /**\n     * Triggers this {@link Challenge}. The ACME server is requested to validate the\n     * response. Note that the validation is performed asynchronously by the ACME server.\n     * <p>\n     * After a challenge is triggered, it changes to {@link Status#PENDING}. As soon as\n     * validation takes place, it changes to {@link Status#PROCESSING}. After validation\n     * the status changes to {@link Status#VALID} or {@link Status#INVALID}, depending on\n     * the outcome of the validation.\n     * <p>\n     * If the challenge requires a resource to be set on your side (e.g. a DNS record or\n     * an HTTP file), it <em>must</em> be reachable from public before {@link #trigger()}\n     * is invoked, and <em>must not</em> be taken down until the challenge has reached\n     * {@link Status#VALID} or {@link Status#INVALID}.\n     * <p>\n     * If this method is invoked a second time, the ACME server is requested to retry the\n     * validation. This can be useful if the client state has changed, for example after a\n     * firewall rule has been updated.\n     *\n     * @see #waitForCompletion(Duration)\n     */\n    public void trigger() throws AcmeException {\n        LOG.debug(\"trigger\");\n        try (var conn = getSession().connect()) {\n            var claims = new JSONBuilder();\n            prepareResponse(claims);\n\n            conn.sendSignedRequest(getLocation(), claims, getLogin());\n            setJSON(conn.readJsonResponse());\n        }\n    }\n\n    /**\n     * Waits until the challenge is completed.\n     * <p>\n     * Is is completed if it reaches either {@link Status#VALID} or\n     * {@link Status#INVALID}.\n     * <p>\n     * This method is synchronous and blocks the current thread.\n     *\n     * @param timeout\n     *         Timeout until a terminal status must have been reached\n     * @return Status that was reached\n     * @since 3.4.0\n     */\n    public Status waitForCompletion(Duration timeout)\n            throws AcmeException, InterruptedException {\n        return waitForStatus(EnumSet.of(Status.VALID, Status.INVALID), timeout);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Dns01Challenge.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.base64UrlEncode;\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.sha256hash;\n\nimport java.io.Serial;\n\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * Implements the {@value TYPE} challenge. It requires a specific DNS record for domain\n * validation. See the acme4j documentation for a detailed explanation.\n */\npublic class Dns01Challenge extends TokenChallenge {\n    @Serial\n    private static final long serialVersionUID = 6964687027713533075L;\n\n    /**\n     * Challenge type name: {@value}\n     */\n    public static final String TYPE = \"dns-01\";\n\n    /**\n     * The prefix of the domain name to be used for the DNS TXT record.\n     */\n    public static final String RECORD_NAME_PREFIX = \"_acme-challenge\";\n\n    /**\n     * Creates a new generic {@link Dns01Challenge} object.\n     *\n     * @param login\n     *         {@link Login} the resource is bound with\n     * @param data\n     *         {@link JSON} challenge data\n     */\n    public Dns01Challenge(Login login, JSON data) {\n        super(login, data);\n    }\n\n    /**\n     * Converts a domain identifier to the Resource Record name to be used for the DNS TXT\n     * record.\n     *\n     * @param identifier\n     *         Domain {@link Identifier} of the domain to be validated\n     * @return Resource Record name (e.g. {@code _acme-challenge.www.example.org.}, note\n     * the trailing full stop character).\n     * @since 4.0.0\n     */\n    public String getRRName(Identifier identifier) {\n        return getRRName(identifier.getDomain());\n    }\n\n    /**\n     * Converts a domain identifier to the Resource Record name to be used for the DNS TXT\n     * record.\n     *\n     * @param domain\n     *         Domain name to be validated\n     * @return Resource Record name (e.g. {@code _acme-challenge.www.example.org.}, note\n     * the trailing full stop character).\n     * @since 4.0.0\n     */\n    public String getRRName(String domain) {\n        return RECORD_NAME_PREFIX + '.' + domain + '.';\n    }\n\n    /**\n     * Returns the digest string to be set in the domain's {@code _acme-challenge} TXT\n     * record.\n     */\n    public String getDigest() {\n        return base64UrlEncode(sha256hash(getAuthorization()));\n    }\n\n    @Override\n    protected boolean acceptable(String type) {\n        return TYPE.equals(type);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/challenge/DnsAccount01Challenge.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2025 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.*;\n\nimport java.io.Serial;\nimport java.net.URL;\nimport java.util.Arrays;\nimport java.util.Locale;\n\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * Implements the {@value TYPE} challenge. It requires a specific DNS record for domain\n * validation. See the acme4j documentation for a detailed explanation.\n *\n * @draft This class is currently based on an RFC draft. It may be changed or removed\n * without notice to reflect future changes to the draft. SemVer rules do not apply here.\n * @since 4.0.0\n */\npublic class DnsAccount01Challenge extends TokenChallenge {\n    @Serial\n    private static final long serialVersionUID = -1098129409378900733L;\n\n    /**\n     * Challenge type name: {@value}\n     */\n    public static final String TYPE = \"dns-account-01\";\n\n    /**\n     * Creates a new generic {@link DnsAccount01Challenge} object.\n     *\n     * @param login\n     *         {@link Login} the resource is bound with\n     * @param data\n     *         {@link JSON} challenge data\n     */\n    public DnsAccount01Challenge(Login login, JSON data) {\n        super(login, data);\n    }\n\n    /**\n     * Converts a domain identifier to the Resource Record name to be used for the DNS TXT\n     * record.\n     *\n     * @param identifier\n     *         {@link Identifier} to be validated\n     * @return Resource Record name (e.g.\n     * {@code _ujmmovf2vn55tgye._acme-challenge.example.org.}, note the trailing full stop\n     * character).\n     */\n    public String getRRName(Identifier identifier) {\n        return getRRName(identifier.getDomain());\n    }\n\n    /**\n     * Converts a domain identifier to the Resource Record name to be used for the DNS TXT\n     * record.\n     *\n     * @param domain\n     *         Domain name to be validated\n     * @return Resource Record name (e.g.\n     * {@code _ujmmovf2vn55tgye._acme-challenge.example.org.}, note the trailing full stop\n     * character).\n     */\n    public String getRRName(String domain) {\n        return getPrefix(getLogin().getAccount().getLocation()) + '.' + domain + '.';\n    }\n\n    /**\n     * Returns the digest string to be set in the domain's TXT record.\n     */\n    public String getDigest() {\n        return base64UrlEncode(sha256hash(getAuthorization()));\n    }\n\n    /**\n     * Returns the prefix of an account location.\n     */\n    private String getPrefix(URL accountLocation) {\n        var urlHash = sha256hash(accountLocation.toExternalForm());\n        var hash = base32Encode(Arrays.copyOfRange(urlHash, 0, 10));\n        return \"_\" + hash.toLowerCase(Locale.ENGLISH)\n                + \"._acme-challenge\";\n    }\n\n    @Override\n    protected boolean acceptable(String type) {\n        return TYPE.equals(type);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/challenge/DnsPersist01Challenge.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2026 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport static java.util.Objects.requireNonNull;\n\nimport java.io.Serial;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.AcmeUtils;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * Implements the {@value TYPE} challenge. It requires a specific DNS record for domain\n * validation. See the acme4j documentation for a detailed explanation.\n *\n * @draft This class is currently based on an RFC draft. It may be changed or removed\n * without notice to reflect future changes to the draft. SemVer rules do not apply here.\n * @since 5.0.0\n */\npublic class DnsPersist01Challenge extends Challenge {\n    @Serial\n    private static final long serialVersionUID = 7532514098897449519L;\n\n    /**\n     * Challenge type name: {@value}\n     */\n    public static final String TYPE = \"dns-persist-01\";\n\n    protected static final String KEY_ISSUER_DOMAIN_NAMES = \"issuer-domain-names\";\n    protected static final String RECORD_NAME_PREFIX = \"_validation-persist\";\n    protected static final String KEY_ACCOUNT_URI = \"accounturi\";\n\n    private static final int ISSUER_SIZE_LIMIT = 10;        // according to the specs\n    private static final int DOMAIN_LENGTH_LIMIT = 253;     // according to the specs\n\n    private @Nullable List<String> issuerDomainNames;\n\n    /**\n     * Creates a new generic {@link DnsPersist01Challenge} object.\n     *\n     * @param login\n     *         {@link Login} the resource is bound with\n     * @param data\n     *         {@link JSON} challenge data\n     */\n    public DnsPersist01Challenge(Login login, JSON data) {\n        super(login, data);\n    }\n\n    /**\n     * Returns the list of issuer-domain-names from the CA. The list is guaranteed to\n     * have at least one element.\n     */\n    public List<String> getIssuerDomainNames() {\n        if (issuerDomainNames == null) {\n            var domainNames = getJSON().get(KEY_ISSUER_DOMAIN_NAMES).asArray().stream()\n                    .map(JSON.Value::asString)\n                    .map(AcmeUtils::toAce)\n                    .toList();\n\n            if (domainNames.isEmpty()) {\n                // malform check is mandatory according to the specification\n                throw new AcmeProtocolException(\"issuer-domain-names missing or empty\");\n            }\n\n            if (domainNames.size() > ISSUER_SIZE_LIMIT) {\n                // malform check is mandatory according to the specification\n                throw new AcmeProtocolException(\"issuer-domain-names size limit exceeded: \"\n                        + domainNames.size() + \" > \" + ISSUER_SIZE_LIMIT);\n            }\n\n            if (domainNames.stream().anyMatch(it -> it.endsWith(\".\"))) {\n                throw new AcmeProtocolException(\"issuer-domain-names must not have trailing dots\");\n            }\n\n            if (!domainNames.stream().allMatch(it -> it.length() <= DOMAIN_LENGTH_LIMIT)) {\n                throw new AcmeProtocolException(\"issuer-domain-names content too long\");\n            }\n\n            issuerDomainNames = domainNames;\n        }\n        return Collections.unmodifiableList(issuerDomainNames);\n    }\n\n    /**\n     * Converts a domain identifier to the Resource Record name to be used for the DNS TXT\n     * record.\n     *\n     * @param identifier\n     *         Domain {@link Identifier} of the domain to be validated\n     * @return Resource Record name (e.g. {@code _validation-persist.www.example.org.},\n     * note the trailing full stop character).\n     */\n    public String getRRName(Identifier identifier) {\n        return getRRName(identifier.getDomain());\n    }\n\n    /**\n     * Converts a domain identifier to the Resource Record name to be used for the DNS TXT\n     * record.\n     *\n     * @param domain\n     *         Domain name to be validated\n     * @return Resource Record name (e.g. {@code _validation-persist.www.example.org.},\n     * note the trailing full stop character).\n     */\n    public String getRRName(String domain) {\n        return RECORD_NAME_PREFIX + '.' + AcmeUtils.toAce(domain) + '.';\n    }\n\n    /**\n     * Returns a builder for the RDATA value of the DNS TXT record.\n     *\n     * @return Builder for the RDATA\n     */\n    public Builder buildRData() {\n        return new Builder(getLogin(), getIssuerDomainNames());\n    }\n\n    /**\n     * Convenience call to get a standard RDATA without optional tags.\n     *\n     * @return RRDATA\n     */\n    public String getRData() {\n        return buildRData().build();\n    }\n\n    /**\n     * Returns the Account URI that is expected to request the validation.\n     *\n     * @since 5.1.0\n     */\n    public URL getAccountUrl() {\n        return getJSON().get(KEY_ACCOUNT_URI).asURL();\n    }\n\n    @Override\n    protected void invalidate() {\n        super.invalidate();\n        issuerDomainNames = null;\n    }\n\n    @Override\n    protected boolean acceptable(String type) {\n        return TYPE.equals(type);\n    }\n\n    @Override\n    protected void setJSON(JSON json) {\n        super.setJSON(json);\n        // TODO: In a future release, KEY_ACCOUNT_URI is expected to be mandatory,\n        //   and this check will always apply!\n        if (getJSON().contains(KEY_ACCOUNT_URI)) {\n            try {\n                var expectedAccount = getJSON().get(KEY_ACCOUNT_URI).asURI();\n                var actualAccount = getLogin().getAccount().getLocation().toURI();\n                if (!actualAccount.equals(expectedAccount)) {\n                    throw new AcmeProtocolException(\"challenge is intended for a different account: \" + expectedAccount);\n                }\n            } catch (URISyntaxException ex) {\n                throw new IllegalStateException(\"Account URL is not an URI?\", ex);\n            }\n        }\n    }\n\n    /**\n     * Builder for RDATA.\n     * <p>\n     * The following default values are assumed unless overridden by one of the builder\n     * methods:\n     * <ul>\n     *     <li>The first issuer domain name from the list of issuer domain names is used</li>\n     *     <li>No wildcard domain</li>\n     *     <li>No persistence limit</li>\n     *     <li>Generate quote-enclosed strings</li>\n     * </ul>\n     */\n    public static class Builder {\n        private final Login login;\n        private final List<String> issuerDomainNames;\n        private String issuer;\n        private boolean wildcard = false;\n        private boolean quotes = true;\n        private @Nullable Instant persistUntil = null;\n\n        private Builder(Login login, List<String> issuerDomainNames) {\n            this.login = login;\n            this.issuerDomainNames = issuerDomainNames;\n            this.issuer = issuerDomainNames.get(0);\n        }\n\n        /**\n         * Change the issuer domain name.\n         *\n         * @param issuer\n         *         Issuer domain name, must be one of\n         *         {@link DnsPersist01Challenge#getIssuerDomainNames()}.\n         */\n        public Builder issuerDomainName(String issuer) {\n            requireNonNull(issuer, \"issuer\");\n            if (!issuerDomainNames.contains(issuer)) {\n                throw new IllegalArgumentException(\"Domain \" + issuer + \" is not in the list of issuer-domain-names\");\n            }\n            this.issuer = issuer;\n            return this;\n        }\n\n        /**\n         * Request wildcard validation.\n         */\n        public Builder wildcard() {\n            wildcard = true;\n            return this;\n        }\n\n        /**\n         * Instant until this RDATA is valid. The CA must not use this record after that.\n         *\n         * @param instant\n         *         Persist until instant\n         */\n        public Builder persistUntil(Instant instant) {\n            persistUntil = requireNonNull(instant, \"instant\");\n            return this;\n        }\n\n        /**\n         * Do not use quote-enclosed strings. Proper formatting of the resulting RDATA\n         * must be done externally!\n         */\n        public Builder noQuotes() {\n            quotes = false;\n            return this;\n        }\n\n        /**\n         * Build the RDATA string for the DNS TXT record.\n         */\n        public String build() {\n            var parts = new ArrayList<String>();\n            parts.add(issuer);\n            parts.add(\"accounturi=\" + login.getAccount().getLocation());\n\n            if (wildcard) {\n                parts.add(\"policy=wildcard\");\n            }\n\n            if (persistUntil != null) {\n                parts.add(\"persistUntil=\" + persistUntil.getEpochSecond());\n            }\n\n            if (quotes) {\n                // Quotes inside the parts should be escaped. However, we don't expect\n                // that any part contains qoutes.\n                return '\"' + String.join(\";\\\" \\\" \", parts) + '\"';\n            } else {\n                return String.join(\"; \", parts);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Http01Challenge.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport java.io.Serial;\n\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * Implements the {@value TYPE} challenge. For domain validation, it requires a specific\n * file that can be retrieved from the domain via HTTP. See the acme4j documentation for a\n * detailed explanation.\n */\npublic class Http01Challenge extends TokenChallenge {\n    @Serial\n    private static final long serialVersionUID = 3322211185872544605L;\n\n    /**\n     * Challenge type name: {@value}\n     */\n    public static final String TYPE = \"http-01\";\n\n    /**\n     * Creates a new generic {@link Http01Challenge} object.\n     *\n     * @param login\n     *            {@link Login} the resource is bound with\n     * @param data\n     *            {@link JSON} challenge data\n     */\n    public Http01Challenge(Login login, JSON data) {\n        super(login, data);\n    }\n\n    /**\n     * Returns the token to be used for this challenge.\n     */\n    @Override\n    public String getToken() {\n        return super.getToken();\n    }\n\n    @Override\n    protected boolean acceptable(String type) {\n        return TYPE.equals(type);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TlsAlpn01Challenge.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.sha256hash;\n\nimport java.io.IOException;\nimport java.io.Serial;\nimport java.security.KeyPair;\nimport java.security.cert.X509Certificate;\n\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.util.CertificateUtils;\n\n/**\n * Implements the {@value TYPE} challenge. It requires a specific certificate that can be\n * retrieved from the domain via HTTPS request. See the acme4j documentation for a\n * detailed explanation.\n *\n * @since 2.1\n */\npublic class TlsAlpn01Challenge extends TokenChallenge {\n    @Serial\n    private static final long serialVersionUID = -5590351078176091228L;\n\n    /**\n     * Challenge type name: {@value}\n     */\n    public static final String TYPE = \"tls-alpn-01\";\n\n    /**\n     * OID of the {@code acmeValidation} extension.\n     */\n    public static final String ACME_VALIDATION_OID = \"1.3.6.1.5.5.7.1.31\";\n\n    /**\n     * {@code acme-tls/1} protocol.\n     */\n    public static final String ACME_TLS_1_PROTOCOL = \"acme-tls/1\";\n\n    /**\n     * Creates a new generic {@link TlsAlpn01Challenge} object.\n     *\n     * @param login\n     *            {@link Login} the resource is bound with\n     * @param data\n     *            {@link JSON} challenge data\n     */\n    public TlsAlpn01Challenge(Login login, JSON data) {\n        super(login, data);\n    }\n\n    /**\n     * Returns the value that is to be used as {@code acmeValidation} extension in\n     * the test certificate.\n     */\n    public byte[] getAcmeValidation() {\n        return sha256hash(getAuthorization());\n    }\n\n    /**\n     * Creates a self-signed {@link X509Certificate} for this challenge. The certificate\n     * is valid for 7 days.\n     *\n     * @param keypair\n     *         A domain {@link KeyPair} to be used for the challenge\n     * @param id\n     *         The {@link Identifier} that is to be validated\n     * @return Created certificate\n     * @since 3.0.0\n     */\n    public X509Certificate createCertificate(KeyPair keypair, Identifier id) {\n        try {\n            return CertificateUtils.createTlsAlpn01Certificate(keypair, id, getAcmeValidation());\n        } catch (IOException ex) {\n            throw new IllegalArgumentException(\"Bad certificate parameters\", ex);\n        }\n    }\n\n    @Override\n    protected boolean acceptable(String type) {\n        return TYPE.equals(type);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.base64UrlEncode;\n\nimport java.io.Serial;\n\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.AcmeUtils;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JoseUtils;\n\n/**\n * A generic extension of {@link Challenge} that handles challenges with a {@code token}\n * and {@code keyAuthorization}.\n */\npublic class TokenChallenge extends Challenge {\n    @Serial\n    private static final long serialVersionUID = 1634133407432681800L;\n\n    protected static final String KEY_TOKEN = \"token\";\n\n    /**\n     * Creates a new generic {@link TokenChallenge} object.\n     *\n     * @param login\n     *            {@link Login} the resource is bound with\n     * @param data\n     *            {@link JSON} challenge data\n     */\n    public TokenChallenge(Login login, JSON data) {\n        super(login, data);\n    }\n\n    /**\n     * Gets the token.\n     */\n    protected String getToken() {\n        var token = getJSON().get(KEY_TOKEN).asString();\n        if (!AcmeUtils.isValidBase64Url(token)) {\n            throw new AcmeProtocolException(\"Invalid token: \" + token);\n        }\n        return token;\n    }\n\n    /**\n     * Computes the key authorization for the given token.\n     * <p>\n     * The default is {@code token + '.' + base64url(jwkThumbprint)}. Subclasses may\n     * override this method if a different algorithm is used.\n     *\n     * @param token\n     *         Token to be used\n     * @return Key Authorization string for that token\n     * @since 2.12\n     */\n    protected String keyAuthorizationFor(String token) {\n        var pk = getLogin().getPublicKey();\n        return token + '.' + base64UrlEncode(JoseUtils.thumbprint(pk));\n    }\n\n    /**\n     * Returns the authorization string.\n     * <p>\n     * The default uses {@link #keyAuthorizationFor(String)} to compute the key\n     * authorization of {@link #getToken()}. Subclasses may override this method if a\n     * different algorithm is used.\n     */\n    public String getAuthorization() {\n        return keyAuthorizationFor(getToken());\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/challenge/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains all standard challenges, as well as base classes for challenges\n * that are proprietary to a CA.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.challenge;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport java.net.URL;\nimport java.security.KeyPair;\nimport java.security.cert.X509Certificate;\nimport java.time.Instant;\nimport java.time.ZonedDateTime;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Optional;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\n\n/**\n * Connects to the ACME server and offers different methods for invoking the API.\n * <p>\n * The actual way of communicating with the ACME server is intentionally left open.\n * Implementations could use other means than HTTP, or could mock the communication for\n * unit testing.\n */\npublic interface Connection extends AutoCloseable {\n\n    /**\n     * Resets the session nonce, by fetching a new one.\n     *\n     * @param session\n     *            {@link Session} instance to fetch a nonce for\n     */\n    void resetNonce(Session session) throws AcmeException;\n\n    /**\n     * Sends a simple GET request.\n     * <p>\n     * If the response code was not HTTP status 200, an {@link AcmeException} matching\n     * the error is raised.\n     *\n     * @param url\n     *            {@link URL} to send the request to.\n     * @param session\n     *            {@link Session} instance to be used for tracking\n     * @param ifModifiedSince\n     *            {@link ZonedDateTime} to be sent as \"If-Modified-Since\" header, or\n     *            {@code null} if this header is not to be used\n     * @return HTTP status that was returned\n     */\n    int sendRequest(URL url, Session session, @Nullable ZonedDateTime ifModifiedSince)\n            throws AcmeException;\n\n    /**\n     * Sends a signed POST-as-GET request for a certificate resource. Requires a\n     * {@link Login} for the session and {@link KeyPair}. The {@link Login} account\n     * location is sent in a \"kid\" protected header.\n     * <p>\n     * If the server does not return a 200 class status code, an {@link AcmeException} is\n     * raised matching the error.\n     *\n     * @param url\n     *            {@link URL} to send the request to.\n     * @param login\n     *            {@link Login} instance to be used for signing and tracking.\n     * @return HTTP 200 class status that was returned\n     */\n    int sendCertificateRequest(URL url, Login login) throws AcmeException;\n\n    /**\n     * Sends a signed POST-as-GET request. Requires a {@link Login} for the session and\n     * {@link KeyPair}. The {@link Login} account location is sent in a \"kid\" protected\n     * header.\n     * <p>\n     * If the server does not return a 200 class status code, an {@link AcmeException} is\n     * raised matching the error.\n     *\n     * @param url\n     *            {@link URL} to send the request to.\n     * @param login\n     *            {@link Login} instance to be used for signing and tracking.\n     * @return HTTP 200 class status that was returned\n     */\n    int sendSignedPostAsGetRequest(URL url, Login login) throws AcmeException;\n\n    /**\n     * Sends a signed POST request. Requires a {@link Login} for the session and\n     * {@link KeyPair}. The {@link Login} account location is sent in a \"kid\" protected\n     * header.\n     * <p>\n     * If the server does not return a 200 class status code, an {@link AcmeException} is\n     * raised matching the error.\n     *\n     * @param url\n     *            {@link URL} to send the request to.\n     * @param claims\n     *            {@link JSONBuilder} containing claims.\n     * @param login\n     *            {@link Login} instance to be used for signing and tracking.\n     * @return HTTP 200 class status that was returned\n     */\n    int sendSignedRequest(URL url, JSONBuilder claims, Login login) throws AcmeException;\n\n    /**\n     * Sends a signed POST request. Only requires a {@link Session}.\n     * <p>\n     * If the server does not return a 200 class status code, an {@link AcmeException} is\n     * raised matching the error.\n     *\n     * @param url\n     *            {@link URL} to send the request to.\n     * @param claims\n     *            {@link JSONBuilder} containing claims.\n     * @param session\n     *            {@link Session} instance to be used for tracking.\n     * @param signer\n     *            {@link RequestSigner} to sign the request with\n     * @return HTTP 200 class status that was returned\n     * @since 5.0.0\n     */\n    int sendSignedRequest(URL url, JSONBuilder claims, Session session, RequestSigner signer)\n                throws AcmeException;\n\n    /**\n     * Reads a server response as JSON object.\n     *\n     * @return The JSON response.\n     */\n    JSON readJsonResponse() throws AcmeException;\n\n    /**\n     * Reads a certificate and its chain of issuers.\n     *\n     * @return List of X.509 certificate and chain that was read.\n     */\n    List<X509Certificate> readCertificates() throws AcmeException;\n\n    /**\n     * Returns the Retry-After header if present.\n     *\n     * @since 3.0.0\n     */\n    Optional<Instant> getRetryAfter();\n\n    /**\n     * Gets the nonce from the nonce header.\n     *\n     * @return Base64 encoded nonce, or empty if no nonce header was set\n     */\n    Optional<String> getNonce();\n\n    /**\n     * Gets a location from the {@code Location} header.\n     * <p>\n     * Relative links are resolved against the last request's URL.\n     *\n     * @return Location {@link URL}\n     * @throws org.shredzone.acme4j.exception.AcmeProtocolException if the location\n     * header is missing\n     */\n    URL getLocation();\n\n    /**\n     * Returns the content of the last-modified header, if present.\n     *\n     * @return Date in the Last-Modified header, or empty if the server did not provide\n     * this information.\n     * @since 2.10\n     */\n    Optional<ZonedDateTime> getLastModified();\n\n    /**\n     * Returns the expiration date of the resource, if present.\n     *\n     * @return Expiration date, either from the Cache-Control or Expires header. If empty,\n     * the server did not provide an expiration date, or forbid caching.\n     * @since 2.10\n     */\n    Optional<ZonedDateTime> getExpiration();\n\n    /**\n     * Gets one or more relation links from the header. The result is expected to be a\n     * URL.\n     * <p>\n     * Relative links are resolved against the last request's URL.\n     *\n     * @param relation\n     *         Link relation\n     * @return Collection of links. Empty if there was no such relation.\n     */\n    Collection<URL> getLinks(String relation);\n\n    /**\n     * Closes the {@link Connection}, releasing all resources.\n     */\n    @Override\n    void close();\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2023 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;\nimport static java.util.function.Predicate.not;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.security.cert.CertificateException;\nimport java.security.cert.CertificateFactory;\nimport java.security.cert.X509Certificate;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.format.DateTimeParseException;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Consumer;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.zip.GZIPInputStream;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Problem;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeNetworkException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.exception.AcmeRateLimitedException;\nimport org.shredzone.acme4j.exception.AcmeServerException;\nimport org.shredzone.acme4j.exception.AcmeUnauthorizedException;\nimport org.shredzone.acme4j.exception.AcmeUserActionRequiredException;\nimport org.shredzone.acme4j.toolbox.AcmeUtils;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Default implementation of {@link Connection}. It communicates with the ACME server via\n * HTTP, with a client that is provided by the given {@link HttpConnector}.\n */\npublic class DefaultConnection implements Connection {\n    private static final Logger LOG = LoggerFactory.getLogger(DefaultConnection.class);\n\n    private static final int HTTP_OK = 200;\n    private static final int HTTP_CREATED = 201;\n    private static final int HTTP_NO_CONTENT = 204;\n    private static final int HTTP_NOT_MODIFIED = 304;\n\n    private static final String ACCEPT_HEADER = \"Accept\";\n    private static final String ACCEPT_CHARSET_HEADER = \"Accept-Charset\";\n    private static final String ACCEPT_LANGUAGE_HEADER = \"Accept-Language\";\n    private static final String ACCEPT_ENCODING_HEADER = \"Accept-Encoding\";\n    private static final String CACHE_CONTROL_HEADER = \"Cache-Control\";\n    private static final String CONTENT_TYPE_HEADER = \"Content-Type\";\n    private static final String DATE_HEADER = \"Date\";\n    private static final String EXPIRES_HEADER = \"Expires\";\n    private static final String IF_MODIFIED_SINCE_HEADER = \"If-Modified-Since\";\n    private static final String LAST_MODIFIED_HEADER = \"Last-Modified\";\n    private static final String LINK_HEADER = \"Link\";\n    private static final String LOCATION_HEADER = \"Location\";\n    private static final String REPLAY_NONCE_HEADER = \"Replay-Nonce\";\n    private static final String RETRY_AFTER_HEADER = \"Retry-After\";\n    private static final String DEFAULT_CHARSET = \"utf-8\";\n    private static final String MIME_JSON = \"application/json\";\n    private static final String MIME_JSON_PROBLEM = \"application/problem+json\";\n    private static final String MIME_CERTIFICATE_CHAIN = \"application/pem-certificate-chain\";\n\n    private static final URI BAD_NONCE_ERROR = URI.create(\"urn:ietf:params:acme:error:badNonce\");\n    private static final int MAX_ATTEMPTS = 10;\n\n    private static final Pattern NO_CACHE_PATTERN = Pattern.compile(\"(?:^|.*?,)\\\\s*no-(?:cache|store)\\\\s*(?:,.*|$)\", Pattern.CASE_INSENSITIVE);\n    private static final Pattern MAX_AGE_PATTERN = Pattern.compile(\"(?:^|.*?,)\\\\s*max-age=(\\\\d+)\\\\s*(?:,.*|$)\", Pattern.CASE_INSENSITIVE);\n    private static final Pattern DIGITS_ONLY_PATTERN = Pattern.compile(\"^\\\\d+$\");\n\n    protected final HttpConnector httpConnector;\n    protected final HttpClient httpClient;\n    protected @Nullable HttpResponse<InputStream> lastResponse;\n\n    /**\n     * Creates a new {@link DefaultConnection}.\n     *\n     * @param httpConnector\n     *         {@link HttpConnector} to be used for HTTP connections\n     */\n    public DefaultConnection(HttpConnector httpConnector) {\n        this.httpConnector = Objects.requireNonNull(httpConnector, \"httpConnector\");\n        this.httpClient = httpConnector.getHttpClient();\n    }\n\n    @Override\n    public void resetNonce(Session session) throws AcmeException {\n        assertConnectionIsClosed();\n\n        try (var nonceHolder = session.lockNonce()) {\n            nonceHolder.setNonce(null);\n\n            var newNonceUrl = session.resourceUrl(Resource.NEW_NONCE);\n\n            LOG.debug(\"HEAD {}\", newNonceUrl);\n\n            sendRequest(session, newNonceUrl, b ->\n                    b.method(\"HEAD\", HttpRequest.BodyPublishers.noBody()));\n\n            logHeaders();\n\n            var rc = getResponse().statusCode();\n            if (rc != HTTP_OK && rc != HTTP_NO_CONTENT) {\n                throw new AcmeException(\"Server responded with HTTP \" + rc + \" while trying to retrieve a nonce\");\n            }\n\n            nonceHolder.setNonce(getNonce()\n                    .orElseThrow(() -> new AcmeProtocolException(\"Server did not provide a nonce\"))\n            );\n        } catch (IOException ex) {\n            throw new AcmeNetworkException(ex);\n        } finally {\n            close();\n        }\n    }\n\n    @Override\n    public int sendRequest(URL url, Session session, @Nullable ZonedDateTime ifModifiedSince)\n            throws AcmeException {\n        Objects.requireNonNull(url, \"url\");\n        Objects.requireNonNull(session, \"session\");\n        assertConnectionIsClosed();\n\n        LOG.debug(\"GET {}\", url);\n\n        try (var nonceHolder = session.lockNonce()) {\n            sendRequest(session, url, builder -> {\n                builder.GET();\n                builder.header(ACCEPT_HEADER, MIME_JSON);\n                if (ifModifiedSince != null) {\n                    builder.header(IF_MODIFIED_SINCE_HEADER, ifModifiedSince.format(RFC_1123_DATE_TIME));\n                }\n            });\n\n            logHeaders();\n\n            getNonce().ifPresent(nonceHolder::setNonce);\n\n            var rc = getResponse().statusCode();\n            if (rc != HTTP_OK && rc != HTTP_CREATED && (rc != HTTP_NOT_MODIFIED || ifModifiedSince == null)) {\n                throwAcmeException();\n            }\n            return rc;\n        } catch (IOException ex) {\n            throw new AcmeNetworkException(ex);\n        }\n    }\n\n    @Override\n    public int sendCertificateRequest(URL url, Login login) throws AcmeException {\n        return sendSignedRequest(url, null, login.getSession(), MIME_CERTIFICATE_CHAIN,\n                login::createJoseRequest);\n    }\n\n    @Override\n    public int sendSignedPostAsGetRequest(URL url, Login login) throws AcmeException {\n        return sendSignedRequest(url, null, login.getSession(), MIME_JSON,\n                login::createJoseRequest);\n    }\n\n    @Override\n    public int sendSignedRequest(URL url, JSONBuilder claims, Login login) throws AcmeException {\n        return sendSignedRequest(url, claims, login.getSession(), MIME_JSON,\n                login::createJoseRequest);\n    }\n\n    @Override\n    public int sendSignedRequest(URL url, JSONBuilder claims, Session session, RequestSigner signer)\n            throws AcmeException {\n        return sendSignedRequest(url, claims, session, MIME_JSON, signer);\n    }\n\n    @Override\n    public JSON readJsonResponse() throws AcmeException {\n        expectContentType(Set.of(MIME_JSON, MIME_JSON_PROBLEM));\n\n        try (var in = getResponseBody()) {\n            var result = JSON.parse(in);\n            LOG.debug(\"Result JSON: {}\", result);\n            return result;\n        } catch (IOException ex) {\n            throw new AcmeNetworkException(ex);\n        }\n    }\n\n    @Override\n    public List<X509Certificate> readCertificates() throws AcmeException {\n        expectContentType(Set.of(MIME_CERTIFICATE_CHAIN));\n\n        try (var in = new TrimmingInputStream(getResponseBody())) {\n            var cf = CertificateFactory.getInstance(\"X.509\");\n            return cf.generateCertificates(in).stream()\n                    .map(X509Certificate.class::cast)\n                    .toList();\n        } catch (IOException ex) {\n            throw new AcmeNetworkException(ex);\n        } catch (CertificateException ex) {\n            throw new AcmeProtocolException(\"Failed to read certificate\", ex);\n        }\n    }\n\n    @Override\n    public Optional<String> getNonce() {\n        var nonceHeaderOpt = getResponse().headers()\n                .firstValue(REPLAY_NONCE_HEADER)\n                .map(String::trim)\n                .filter(not(String::isEmpty));\n        if (nonceHeaderOpt.isPresent()) {\n            var nonceHeader = nonceHeaderOpt.get();\n\n            if (!AcmeUtils.isValidBase64Url(nonceHeader)) {\n                throw new AcmeProtocolException(\"Invalid replay nonce: \" + nonceHeader);\n            }\n\n            LOG.debug(\"Replay Nonce: {}\", nonceHeader);\n        }\n        return nonceHeaderOpt;\n    }\n\n    @Override\n    public URL getLocation() {\n        return getResponse().headers()\n                .firstValue(LOCATION_HEADER)\n                .map(l -> {\n                    LOG.debug(\"Location: {}\", l);\n                    return l;\n                })\n                .map(this::resolveRelative)\n                .orElseThrow(() -> new AcmeProtocolException(\"location header is missing\"));\n    }\n\n    @Override\n    public Optional<ZonedDateTime> getLastModified() {\n        return getResponse().headers()\n                .firstValue(LAST_MODIFIED_HEADER)\n                .map(lm -> {\n                    try {\n                        return ZonedDateTime.parse(lm, RFC_1123_DATE_TIME);\n                    } catch (DateTimeParseException ex) {\n                        LOG.debug(\"Ignored invalid Last-Modified date: {}\", lm, ex);\n                        return null;\n                    }\n                });\n    }\n\n    @Override\n    public Optional<ZonedDateTime> getExpiration() {\n        var cacheControlHeader = getResponse().headers()\n                .firstValue(CACHE_CONTROL_HEADER)\n                .filter(not(h -> NO_CACHE_PATTERN.matcher(h).matches()))\n                .map(MAX_AGE_PATTERN::matcher)\n                .filter(Matcher::matches)\n                .map(m -> Integer.parseInt(m.group(1)))\n                .filter(maxAge -> maxAge != 0)\n                .map(maxAge -> ZonedDateTime.now(ZoneId.of(\"UTC\")).plusSeconds(maxAge));\n\n        if (cacheControlHeader.isPresent()) {\n            return cacheControlHeader;\n        }\n\n        return getResponse().headers()\n                .firstValue(EXPIRES_HEADER)\n                .flatMap(header -> {\n                    try {\n                        return Optional.of(ZonedDateTime.parse(header, RFC_1123_DATE_TIME));\n                    } catch (DateTimeParseException ex) {\n                        LOG.debug(\"Ignored invalid Expires date: {}\", header, ex);\n                        return Optional.empty();\n                    }\n                });\n    }\n\n    @Override\n    public Collection<URL> getLinks(String relation) {\n        return collectLinks(relation).stream()\n                .map(this::resolveRelative)\n                .toList();\n    }\n\n    @Override\n    public void close() {\n        lastResponse = null;\n    }\n\n    /**\n     * Sends a HTTP request via http client. This is the central method to be used for\n     * sending. It will create a {@link HttpRequest} by using the request builder,\n     * configure commnon headers, and then send the request via {@link HttpClient}.\n     *\n     * @param session\n     *         {@link Session} to be used for sending\n     * @param url\n     *         Target {@link URL}\n     * @param body\n     *         Callback that completes the {@link HttpRequest.Builder} with the request\n     *         body (e.g. HTTP method, request body, more headers).\n     */\n    protected void sendRequest(Session session, URL url, Consumer<HttpRequest.Builder> body) throws IOException {\n        try {\n            var builder = httpConnector.createRequestBuilder(url)\n                    .header(ACCEPT_CHARSET_HEADER, DEFAULT_CHARSET)\n                    .header(ACCEPT_LANGUAGE_HEADER, session.getLanguageHeader());\n\n            if (session.networkSettings().isCompressionEnabled()) {\n                builder.header(ACCEPT_ENCODING_HEADER, \"gzip\");\n            }\n\n            body.accept(builder);\n\n            lastResponse = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofInputStream());\n        } catch (InterruptedException ex) {\n            throw new IOException(\"Request was interrupted\", ex);\n        }\n    }\n\n    /**\n     * Sends a signed POST request.\n     *\n     * @param url\n     *         {@link URL} to send the request to.\n     * @param claims\n     *         {@link JSONBuilder} containing claims. {@code null} for POST-as-GET\n     *         request.\n     * @param accept\n     *         Accept header\n     * @return HTTP 200 class status that was returned\n     */\n    protected int sendSignedRequest(URL url, @Nullable JSONBuilder claims,\n                                    Session session, String accept, RequestSigner signer)\n            throws AcmeException {\n        Objects.requireNonNull(url, \"url\");\n        Objects.requireNonNull(session, \"session\");\n        Objects.requireNonNull(accept, \"accept\");\n        Objects.requireNonNull(signer, \"signer\");\n        assertConnectionIsClosed();\n\n        var attempt = 1;\n        while (true) {\n            try {\n                return performRequest(url, claims, session, accept, signer);\n            } catch (AcmeServerException ex) {\n                if (!BAD_NONCE_ERROR.equals(ex.getType())) {\n                    throw ex;\n                }\n                if (attempt == MAX_ATTEMPTS) {\n                    throw ex;\n                }\n                LOG.info(\"Bad Replay Nonce, trying again (attempt {}/{})\", attempt, MAX_ATTEMPTS);\n                attempt++;\n            }\n        }\n    }\n\n    /**\n     * Performs the POST request.\n     *\n     * @param url\n     *         {@link URL} to send the request to.\n     * @param claims\n     *         {@link JSONBuilder} containing claims. {@code null} for POST-as-GET\n     *         request.\n     * @param accept\n     *         Accept header\n     * @return HTTP 200 class status that was returned\n     */\n    private int performRequest(URL url, @Nullable JSONBuilder claims, Session session,\n                               String accept, RequestSigner signer) throws AcmeException {\n        try (var nonceHolder = session.lockNonce()) {\n            if (nonceHolder.getNonce() == null) {\n                resetNonce(session);\n            }\n\n            var jose = signer.createRequest(url, claims, nonceHolder.getNonce());\n            var outputData = jose.toString();\n\n            sendRequest(session, url, builder -> {\n                builder.POST(HttpRequest.BodyPublishers.ofString(outputData));\n                builder.header(ACCEPT_HEADER, accept);\n                builder.header(CONTENT_TYPE_HEADER, \"application/jose+json\");\n            });\n\n            logHeaders();\n\n            nonceHolder.setNonce(getNonce().orElse(null));\n\n            var rc = getResponse().statusCode();\n            if (rc != HTTP_OK && rc != HTTP_CREATED) {\n                throwAcmeException();\n            }\n            return rc;\n        } catch (IOException ex) {\n            throw new AcmeNetworkException(ex);\n        }\n    }\n\n    @Override\n    public Optional<Instant> getRetryAfter() {\n        return getResponse().headers()\n                .firstValue(RETRY_AFTER_HEADER)\n                .map(this::parseRetryAfterHeader);\n    }\n\n    /**\n     * Parses the content of a Retry-After header. The header can either contain a\n     * relative or an absolute time.\n     *\n     * @param header\n     *         Retry-After header\n     * @return Instant given in the header\n     * @throws AcmeProtocolException\n     *         if the header content is invalid\n     */\n    private Instant parseRetryAfterHeader(String header) {\n        // See RFC 2616 section 14.37\n        try {\n            // delta-seconds\n            if (DIGITS_ONLY_PATTERN.matcher(header).matches()) {\n                var delta = Integer.parseInt(header);\n                var date = getResponse().headers().firstValue(DATE_HEADER)\n                        .map(d -> ZonedDateTime.parse(d, RFC_1123_DATE_TIME).toInstant())\n                        .orElseGet(Instant::now);\n                return date.plusSeconds(delta);\n            }\n\n            // HTTP-date\n            return ZonedDateTime.parse(header, RFC_1123_DATE_TIME).toInstant();\n        } catch (RuntimeException ex) {\n            throw new AcmeProtocolException(\"Bad retry-after header value: \" + header, ex);\n        }\n    }\n\n    /**\n     * Provides an {@link InputStream} of the response body. If the stream is compressed,\n     * it will also take care for decompression.\n     */\n    private InputStream getResponseBody() throws IOException {\n        var stream = getResponse().body();\n        if (stream == null) {\n            throw new AcmeProtocolException(\"Unexpected empty response\");\n        }\n\n        if (getResponse().headers().firstValue(\"Content-Encoding\")\n                .filter(\"gzip\"::equalsIgnoreCase)\n                .isPresent()) {\n            stream = new GZIPInputStream(stream);\n        }\n\n        return stream;\n    }\n\n    /**\n     * Throws an {@link AcmeException}. This method throws an exception that tries to\n     * explain the error as precisely as possible.\n     */\n    private void throwAcmeException() throws AcmeException {\n        try {\n            if (getResponse().headers().firstValue(CONTENT_TYPE_HEADER)\n                    .map(AcmeUtils::getContentType)\n                    .filter(MIME_JSON_PROBLEM::equals)\n                    .isEmpty()) {\n                // Generic HTTP error\n                throw new AcmeException(\"HTTP \" + getResponse().statusCode());\n            }\n\n            var problem = new Problem(readJsonResponse(), getResponse().request().uri().toURL());\n\n            var error = AcmeUtils.stripErrorPrefix(problem.getType().toString());\n\n            if (\"unauthorized\".equals(error)) {\n                throw new AcmeUnauthorizedException(problem);\n            }\n\n            if (\"userActionRequired\".equals(error)) {\n                var tos = collectLinks(\"terms-of-service\").stream()\n                        .findFirst()\n                        .map(this::resolveUri)\n                        .orElse(null);\n                throw new AcmeUserActionRequiredException(problem, tos);\n            }\n\n            if (\"rateLimited\".equals(error)) {\n                var retryAfter = getRetryAfter();\n                var rateLimits = getLinks(\"help\");\n                throw new AcmeRateLimitedException(problem, retryAfter.orElse(null), rateLimits);\n            }\n\n            throw new AcmeServerException(problem);\n        } catch (IOException ex) {\n            throw new AcmeNetworkException(ex);\n        }\n    }\n\n    /**\n     * Checks if the returned content type is in the list of expected types.\n     *\n     * @param expectedTypes\n     *         content types that are accepted\n     * @throws AcmeProtocolException\n     *         if the returned content type is different\n     */\n    private void expectContentType(Set<String> expectedTypes) {\n        var contentType = getResponse().headers()\n                .firstValue(CONTENT_TYPE_HEADER)\n                .map(AcmeUtils::getContentType)\n                .orElseThrow(() -> new AcmeProtocolException(\"No content type header found\"));\n        if (!expectedTypes.contains(contentType)) {\n            throw new AcmeProtocolException(\"Unexpected content type: \" + contentType);\n        }\n    }\n\n    /**\n     * Returns the response of the last request. If there is no connection currently\n     * open, an exception is thrown instead.\n     * <p>\n     * Note that the response provides an {@link InputStream} that can be read only\n     * once.\n     */\n    private HttpResponse<InputStream> getResponse() {\n        if (lastResponse == null) {\n            throw new IllegalStateException(\"Not connected.\");\n        }\n        return lastResponse;\n    }\n\n    /**\n     * Asserts that the connection is currently closed. Throws an exception if not.\n     */\n    private void assertConnectionIsClosed() {\n        if (lastResponse != null) {\n            throw new IllegalStateException(\"Previous connection is not closed.\");\n        }\n    }\n\n    /**\n     * Log all HTTP headers in debug mode.\n     */\n    private void logHeaders() {\n        if (!LOG.isDebugEnabled()) {\n            return;\n        }\n\n        getResponse().headers().map().forEach((key, headers) ->\n                headers.forEach(value ->\n                        LOG.debug(\"HEADER {}: {}\", key, value)\n                )\n        );\n    }\n\n    /**\n     * Collects links of the given relation.\n     *\n     * @param relation\n     *         Link relation\n     * @return Collection of links, unconverted\n     */\n    private Collection<String> collectLinks(String relation) {\n        var p = Pattern.compile(\"<([^>]+)>\\\\s*;[^<]*?\\\\brel=\\\"?\" + Pattern.quote(relation) + \"\\\"?(?:[\\\\s,;]|$)\");\n        \n        return getResponse().headers().allValues(LINK_HEADER)\n                .stream()\n                .map(p::matcher)\n                .flatMap(Matcher::results)\n                .map(m -> m.group(1))\n                .peek(location -> LOG.debug(\"Link: {} -> {}\", relation, location))\n                .toList();\n    }\n\n    /**\n     * Resolves a relative link against the connection's last URL.\n     *\n     * @param link\n     *         Link to resolve. Absolute links are just converted to an URL.\n     * @return Absolute URL of the given link\n     */\n    private URL resolveRelative(String link) {\n        try {\n            return resolveUri(link).toURL();\n        } catch (MalformedURLException ex) {\n            throw new AcmeProtocolException(\"Cannot resolve relative link: \" + link, ex);\n        }\n    }\n\n    /**\n     * Resolves a relative URI against the connection's last URL.\n     *\n     * @param uri\n     *         URI to resolve\n     * @return Absolute URI of the given link\n     */\n    private URI resolveUri(String uri) {\n        return getResponse().request().uri().resolve(uri);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/connector/HttpConnector.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.util.Properties;\n\nimport edu.umd.cs.findbugs.annotations.SuppressFBWarnings;\nimport org.slf4j.LoggerFactory;\n\n/**\n * A generic HTTP connector. It creates {@link HttpRequest.Builder} that can be\n * individually customized according to the user's needs.\n *\n * @since 3.0.0\n */\npublic class HttpConnector {\n    private static final String USER_AGENT;\n\n    private final NetworkSettings networkSettings;\n    private final HttpClient httpClient;\n\n    static {\n        var agent = new StringBuilder(\"acme4j\");\n\n        try (var in = HttpConnector.class.getResourceAsStream(\"/org/shredzone/acme4j/version.properties\")) {\n            var prop = new Properties();\n            prop.load(in);\n            agent.append('/').append(prop.getProperty(\"version\"));\n        } catch (Exception ex) {\n            // Ignore, just don't use a version\n            LoggerFactory.getLogger(HttpConnector.class).warn(\"Could not read library version\", ex);\n        }\n\n        agent.append(\" Java/\").append(System.getProperty(\"java.version\"));\n        USER_AGENT = agent.toString();\n    }\n\n    /**\n     * Returns the default User-Agent to be used.\n     *\n     * @return User-Agent\n     */\n    public static String defaultUserAgent() {\n        return USER_AGENT;\n    }\n\n    /**\n     * Creates a new {@link HttpConnector} that is using the given\n     * {@link NetworkSettings} and {@link HttpClient}.\n     *\n     * @param networkSettings Network settings to use\n     * @param httpClient HTTP client to use for requests\n     * @since 4.0.0\n     */\n    @SuppressFBWarnings(\"EI_EXPOSE_REP2\")   // behavior is intended\n    public HttpConnector(NetworkSettings networkSettings, HttpClient httpClient) {\n        this.networkSettings = networkSettings;\n        this.httpClient = httpClient;\n    }\n\n    /**\n     * Creates a new {@link HttpRequest.Builder} that is preconfigured and bound to the\n     * given URL. Subclasses can override this method to extend the configuration, or\n     * create a different builder.\n     *\n     * @param url\n     *            {@link URL} to connect to\n     * @return {@link HttpRequest.Builder} connected to the {@link URL}\n     */\n    public HttpRequest.Builder createRequestBuilder(URL url) {\n        try {\n            return HttpRequest.newBuilder(url.toURI())\n                    .header(\"User-Agent\", USER_AGENT)\n                    .timeout(networkSettings.getTimeout());\n        } catch (URISyntaxException ex) {\n            throw new IllegalArgumentException(\"Invalid URL\", ex);\n        }\n    }\n\n    /**\n     * Returns the {@link HttpClient} instance used by this connector.\n     *\n     * @return {@link HttpClient} instance\n     * @since 4.0.0\n     */\n    public HttpClient getHttpClient() {\n        return httpClient;\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/connector/NetworkSettings.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2019 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport java.net.Authenticator;\nimport java.net.ProxySelector;\nimport java.net.http.HttpClient;\nimport java.time.Duration;\nimport java.util.Optional;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Contains network settings to be used for network connections.\n *\n * @since 2.8\n */\npublic class NetworkSettings {\n\n    /**\n     * Name of the system property to control GZIP compression. Expects a boolean value.\n     */\n    public static final String GZIP_PROPERTY_NAME = \"org.shredzone.acme4j.gzip_compression\";\n\n    private ProxySelector proxySelector = HttpClient.Builder.NO_PROXY;\n    private Duration timeout = Duration.ofSeconds(30);\n    private @Nullable Authenticator authenticator = null;\n    private boolean compression = true;\n\n    public NetworkSettings() {\n        try {\n            Optional.ofNullable(System.getProperty(GZIP_PROPERTY_NAME))\n                    .map(Boolean::parseBoolean)\n                    .ifPresent(val -> compression = val);\n        } catch (Exception ex) {\n            // Ignore a broken property name or a SecurityException\n            LoggerFactory.getLogger(NetworkSettings.class)\n                    .warn(\"Could not read system property: {}\", GZIP_PROPERTY_NAME, ex);\n        }\n    }\n\n    /**\n     * Gets the {@link ProxySelector} to be used for connections.\n     *\n     * @since 3.0.0\n     */\n    public ProxySelector getProxySelector() {\n        return proxySelector;\n    }\n\n    /**\n     * Sets a {@link ProxySelector} that is to be used for all connections. If\n     * {@code null}, {@link HttpClient.Builder#NO_PROXY} is used, which is also the\n     * default.\n     *\n     * @since 3.0.0\n     */\n    public void setProxySelector(@Nullable ProxySelector proxySelector) {\n        this.proxySelector = proxySelector != null ? proxySelector : HttpClient.Builder.NO_PROXY;\n    }\n\n    /**\n     * Gets the {@link Authenticator} to be used, or {@code null} if none is to be set.\n     *\n     * @since 3.0.0\n     */\n    public @Nullable Authenticator getAuthenticator() {\n        return authenticator;\n    }\n\n    /**\n     * Sets an {@link Authenticator} to be used if HTTP authentication is needed (e.g.\n     * by a proxy). {@code null} means that no authenticator shall be set.\n     *\n     * @since 3.0.0\n     */\n    public void setAuthenticator(@Nullable Authenticator authenticator) {\n        this.authenticator = authenticator;\n    }\n\n    /**\n     * Gets the current network timeout.\n     */\n    public Duration getTimeout() {\n        return timeout;\n    }\n\n    /**\n     * Sets the network timeout to be used for connections. Defaults to 10 seconds.\n     *\n     * @param timeout\n     *         Network timeout {@link Duration}\n     */\n    public void setTimeout(Duration timeout) {\n        if (timeout == null || timeout.isNegative() || timeout.isZero()) {\n            throw new IllegalArgumentException(\"Timeout must be positive\");\n        }\n\n        this.timeout = timeout;\n    }\n\n    /**\n     * Checks if HTTP compression is enabled.\n     *\n     * @since 3.0.0\n     */\n    public boolean isCompressionEnabled() {\n        return compression;\n    }\n\n    /**\n     * Sets if HTTP compression is enabled. It is enabled by default, but can be\n     * disabled e.g. for debugging purposes.\n     * <p>\n     * acme4j gzip compression can also be controlled via the {@value #GZIP_PROPERTY_NAME}\n     * system property.\n     *\n     * @since 3.0.0\n     */\n    public void setCompressionEnabled(boolean compression) {\n        this.compression = compression;\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/connector/NonceHolder.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2026 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\n\n/**\n * Keeps the current nonce for a request. Make sure that the {@link #close()} method is\n * always invoked, otherwise the related {@link org.shredzone.acme4j.Session} will be\n * blocked.\n * <p>\n * This object is for internal use only.\n *\n * @since 4.0.0\n */\npublic interface NonceHolder extends AutoCloseable {\n    /**\n     * Gets the last base64 encoded nonce, or {@code null} if the session is new.\n     */\n    @Nullable\n    String getNonce();\n\n    /**\n     * Sets the base64 encoded nonce received by the server.\n     */\n    void setNonce(@Nullable String nonce);\n\n    /**\n     * Closes the NonceHolder. Must be invoked!\n     */\n    void close();\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/connector/RequestSigner.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2026 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport java.net.URL;\nimport java.security.KeyPair;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\n\n/**\n * Function for assembling and signing an ACME JOSE request.\n *\n * @since 5.0.0\n */\n@FunctionalInterface\npublic interface RequestSigner {\n\n    /**\n     * Creates an ACME JOSE request.\n     * <p>\n     * Implementors can use\n     * {@link org.shredzone.acme4j.toolbox.JoseUtils#createJoseRequest(URL, KeyPair,\n     * JSONBuilder, String, String)} without giving the signing {@link KeyPair} out of\n     * their control.\n     *\n     * @param url\n     *         {@link URL} of the ACME call\n     * @param payload\n     *         ACME JSON payload. If {@code null}, a POST-as-GET request is generated\n     *         instead.\n     * @param nonce\n     *         Nonce to be used. {@code null} if no nonce is to be used in the JOSE\n     *         header.\n     * @return JSON structure of the JOSE request, ready to be sent.\n     * @see org.shredzone.acme4j.toolbox.JoseUtils#createJoseRequest(URL, KeyPair,\n     * JSONBuilder, String, String)\n     */\n    JSONBuilder createRequest(URL url, @Nullable JSONBuilder payload, @Nullable String nonce);\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/connector/Resource.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\n/**\n * Enumeration of standard resources, and their key name in the CA's directory.\n */\npublic enum Resource {\n\n    NEW_NONCE(\"newNonce\"),\n    NEW_ACCOUNT(\"newAccount\"),\n    NEW_ORDER(\"newOrder\"),\n    NEW_AUTHZ(\"newAuthz\"),\n    REVOKE_CERT(\"revokeCert\"),\n    KEY_CHANGE(\"keyChange\"),\n    RENEWAL_INFO(\"renewalInfo\");\n\n    private final String path;\n\n    Resource(String path) {\n        this.path = path;\n    }\n\n    /**\n     * Returns the key name in the directory.\n     *\n     * @return key name\n     */\n    public String path() {\n        return path;\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/connector/ResourceIterator.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport static java.util.Objects.requireNonNull;\n\nimport java.net.URL;\nimport java.util.ArrayDeque;\nimport java.util.Deque;\nimport java.util.Iterator;\nimport java.util.NoSuchElementException;\nimport java.util.function.BiFunction;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.acme4j.AcmeResource;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * An {@link Iterator} that fetches a batch of URLs from the ACME server, and generates\n * {@link AcmeResource} instances.\n *\n * @param <T>\n *            {@link AcmeResource} type to iterate over\n */\npublic class ResourceIterator<T extends AcmeResource> implements Iterator<T> {\n\n    private final Login login;\n    private final String field;\n    private final Deque<URL> urlList = new ArrayDeque<>();\n    private final BiFunction<Login, URL, T> creator;\n    private boolean eol = false;\n    private @Nullable URL nextUrl;\n\n    /**\n     * Creates a new {@link ResourceIterator}.\n     *\n     * @param login\n     *            {@link Login} to bind this iterator to\n     * @param field\n     *            Field name to be used in the JSON response\n     * @param start\n     *            URL of the first JSON array, may be {@code null} for an empty iterator\n     * @param creator\n     *            Creator for an {@link AcmeResource} that is bound to the given\n     *            {@link Login} and {@link URL}.\n     */\n    public ResourceIterator(Login login, String field, @Nullable URL start, BiFunction<Login, URL, T> creator) {\n        this.login = requireNonNull(login, \"login\");\n        this.field = requireNonNull(field, \"field\");\n        this.nextUrl = start;\n        this.creator = requireNonNull(creator, \"creator\");\n    }\n\n    /**\n     * Checks if there is another object in the result.\n     *\n     * @throws AcmeProtocolException\n     *             if the next batch of URLs could not be fetched from the server\n     */\n    @Override\n    public boolean hasNext() {\n        if (eol) {\n            return false;\n        }\n\n        if (urlList.isEmpty()) {\n            fetch();\n        }\n\n        if (urlList.isEmpty()) {\n            eol = true;\n        }\n\n        return !urlList.isEmpty();\n    }\n\n    /**\n     * Returns the next object of the result.\n     *\n     * @throws AcmeProtocolException\n     *             if the next batch of URLs could not be fetched from the server\n     * @throws NoSuchElementException\n     *             if there are no more entries\n     */\n    @Override\n    public T next() {\n        if (!eol && urlList.isEmpty()) {\n            fetch();\n        }\n\n        var next = urlList.poll();\n        if (next == null) {\n            eol = true;\n            throw new NoSuchElementException(\"no more \" + field);\n        }\n\n        return creator.apply(login, next);\n    }\n\n    /**\n     * Unsupported operation, only here to satisfy the {@link Iterator} interface.\n     */\n    @Override\n    public void remove() {\n        throw new UnsupportedOperationException(\"cannot remove \" + field);\n    }\n\n    /**\n     * Fetches the next batch of URLs. Handles exceptions. Does nothing if there is no\n     * URL of the next batch.\n     */\n    private void fetch() {\n        if (nextUrl == null) {\n            return;\n        }\n\n        try {\n            readAndQueue();\n        } catch (AcmeException ex) {\n            throw new AcmeProtocolException(\"failed to read next set of \" + field, ex);\n        }\n    }\n\n    /**\n     * Reads the next batch of URLs from the server, and fills the queue with the URLs. If\n     * there is a \"next\" header, it is used for the next batch of URLs.\n     */\n    private void readAndQueue() throws AcmeException {\n        var session = login.getSession();\n        try (var conn = session.connect()) {\n            conn.sendSignedPostAsGetRequest(requireNonNull(nextUrl), login);\n            fillUrlList(conn.readJsonResponse());\n\n            nextUrl = conn.getLinks(\"next\").stream().findFirst().orElse(null);\n        }\n    }\n\n    /**\n     * Fills the url list with the URLs found in the desired field.\n     *\n     * @param json\n     *            JSON map to read from\n     */\n    private void fillUrlList(JSON json) {\n        json.get(field).asArray().stream()\n                .map(JSON.Value::asURL)\n                .forEach(urlList::add);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/connector/TrimmingInputStream.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport java.io.BufferedInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Normalizes line separators in an InputStream. Converts all line separators to '\\n'.\n * Multiple line separators are compressed to a single line separator. Leading line\n * separators are removed. Trailing line separators are compressed to a single separator.\n */\npublic class TrimmingInputStream extends InputStream {\n    private final BufferedInputStream in;\n    private boolean startOfFile = true;\n\n    /**\n     * Creates a new {@link TrimmingInputStream}.\n     *\n     * @param in\n     *            {@link InputStream} to read from. Will be closed when this stream is\n     *            closed.\n     */\n    public TrimmingInputStream(InputStream in) {\n        this.in = new BufferedInputStream(in, 1024);\n    }\n\n    @Override\n    public int read() throws IOException {\n        var ch = in.read();\n\n        if (!isLineSeparator(ch)) {\n            startOfFile = false;\n            return ch;\n        }\n\n        in.mark(1);\n        ch = in.read();\n        while (isLineSeparator(ch)) {\n            in.mark(1);\n            ch = in.read();\n        }\n\n        if (startOfFile) {\n            startOfFile = false;\n            return ch;\n        } else {\n            in.reset();\n            return '\\n';\n        }\n    }\n\n    @Override\n    public int available() throws IOException {\n        // Workaround for https://github.com/google/conscrypt/issues/1068. Conscrypt\n        // requires the stream to have at least one non-blocking byte available for\n        // reading, otherwise generateCertificates() will not read the stream, but\n        // immediately returns an empty list. This workaround pre-fills the buffer\n        // of the BufferedInputStream by reading 1 byte ahead.\n        if (in.available() == 0) {\n            in.mark(1);\n            var read = in.read();\n            in.reset();\n            if (read < 0) {\n                return 0;\n            }\n        }\n        return in.available();\n    }\n\n    @Override\n    public void close() throws IOException {\n        in.close();\n        super.close();\n    }\n\n    /**\n     * Checks if the character is a line separator.\n     */\n    private static boolean isLineSeparator(int ch) {\n        return ch == '\\n' || ch == '\\r';\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/connector/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains internal classes for connection to the CA, and for handling the\n * requests and responses.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.connector;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeException.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport java.io.Serial;\n\n/**\n * The root class of all checked acme4j exceptions.\n */\npublic class AcmeException extends Exception {\n    @Serial\n    private static final long serialVersionUID = -2935088954705632025L;\n\n    /**\n     * Creates a generic {@link AcmeException}.\n     */\n    public AcmeException() {\n        super();\n    }\n\n    /**\n     * Creates a generic {@link AcmeException}.\n     *\n     * @param msg\n     *            Description\n     */\n    public AcmeException(String msg) {\n        super(msg);\n    }\n\n    /**\n     * Creates a generic {@link AcmeException}.\n     *\n     * @param msg\n     *            Description\n     * @param cause\n     *            {@link Throwable} that caused this exception\n     */\n    public AcmeException(String msg, Throwable cause) {\n        super(msg, cause);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeLazyLoadingException.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport static java.util.Objects.requireNonNull;\n\nimport java.io.Serial;\nimport java.net.URL;\n\nimport org.shredzone.acme4j.AcmeResource;\n\n/**\n * A runtime exception that is thrown when an {@link AcmeException} occured while trying\n * to lazy-load a resource from the ACME server. It contains the original cause of the\n * exception and a reference to the resource that could not be lazy-loaded. It is usually\n * thrown by getter methods, so the API is not polluted with checked exceptions.\n */\npublic class AcmeLazyLoadingException extends RuntimeException {\n    @Serial\n    private static final long serialVersionUID = 1000353433913721901L;\n\n    private final Class<? extends AcmeResource> type;\n    private final URL location;\n\n    /**\n     * Creates a new {@link AcmeLazyLoadingException}.\n     *\n     * @param resource\n     *            {@link AcmeResource} to be loaded\n     * @param cause\n     *            {@link AcmeException} that was raised\n     */\n    public AcmeLazyLoadingException(AcmeResource resource, AcmeException cause) {\n        this(requireNonNull(resource).getClass(), requireNonNull(resource).getLocation(), cause);\n    }\n\n    /**\n     * Creates a new {@link AcmeLazyLoadingException}.\n     * <p>\n     * This constructor is used if there is no actual instance of the resource.\n     *\n     * @param type\n     *         {@link AcmeResource} type to be loaded\n     * @param location\n     *         Resource location\n     * @param cause\n     *         {@link AcmeException} that was raised\n     * @since 2.8\n     */\n    public AcmeLazyLoadingException(Class<? extends AcmeResource> type, URL location, AcmeException cause) {\n        super(requireNonNull(type).getSimpleName() + \" \" + requireNonNull(location), requireNonNull(cause));\n        this.type = type;\n        this.location = location;\n    }\n\n    /**\n     * Returns the {@link AcmeResource} type of the resource that could not be loaded.\n     */\n    public Class<? extends AcmeResource> getType() {\n        return type;\n    }\n\n    /**\n     * Returns the location of the resource that could not be loaded.\n     */\n    public URL getLocation() {\n        return location;\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeNetworkException.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport java.io.IOException;\nimport java.io.Serial;\n\n/**\n * A general network error has occured while communicating with the server (e.g. network\n * timeout).\n */\npublic class AcmeNetworkException extends AcmeException {\n    @Serial\n    private static final long serialVersionUID = 2054398693543329179L;\n\n    /**\n     * Create a new {@link AcmeNetworkException}.\n     *\n     * @param cause\n     *            {@link IOException} that caused the network error\n     */\n    public AcmeNetworkException(IOException cause) {\n        super(\"Network error\", cause);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeNotSupportedException.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2023 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport java.io.Serial;\n\n/**\n * A runtime exception that is thrown if the ACME server does not support a certain\n * feature. It might be either because that feature is optional, or because the server\n * is not fully RFC compliant.\n */\npublic class AcmeNotSupportedException extends AcmeProtocolException {\n    @Serial\n    private static final long serialVersionUID = 3434074002226584731L;\n\n    /**\n     * Creates a new {@link AcmeNotSupportedException}.\n     *\n     * @param feature\n     *            Feature that is not supported\n     */\n    public AcmeNotSupportedException(String feature) {\n        super(\"Server does not support \" + feature);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeProtocolException.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport java.io.Serial;\n\n/**\n * A runtime exception that is thrown when the response of the server is violating the\n * RFC, and could not be handled or parsed for that reason. It is an indicator that the CA\n * does not fully comply with the RFC, and is usually not expected to be thrown.\n */\npublic class AcmeProtocolException extends RuntimeException {\n    @Serial\n    private static final long serialVersionUID = 2031203835755725193L;\n\n    /**\n     * Creates a new {@link AcmeProtocolException}.\n     *\n     * @param msg\n     *            Reason of the exception\n     */\n    public AcmeProtocolException(String msg) {\n        super(msg);\n    }\n\n    /**\n     * Creates a new {@link AcmeProtocolException}.\n     *\n     * @param msg\n     *            Reason of the exception\n     * @param cause\n     *            Cause\n     */\n    public AcmeProtocolException(String msg, Throwable cause) {\n        super(msg, cause);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitedException.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport java.io.Serial;\nimport java.net.URL;\nimport java.time.Instant;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Optional;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.acme4j.Problem;\n\n/**\n * A rate limit was exceeded. If provided by the server, it also includes the earliest\n * time at which a new attempt will be accepted, and a reference to a document that\n * further explains the rate limit that was exceeded.\n */\npublic class AcmeRateLimitedException extends AcmeServerException {\n    @Serial\n    private static final long serialVersionUID = 4150484059796413069L;\n\n    private final @Nullable Instant retryAfter;\n    private final Collection<URL> documents;\n\n    /**\n     * Creates a new {@link AcmeRateLimitedException}.\n     *\n     * @param problem\n     *         {@link Problem} that caused the exception\n     * @param retryAfter\n     *         The instant of time that the request is expected to succeed again, may be\n     *         {@code null} if not known\n     * @param documents\n     *         URLs pointing to documents about the rate limit that was hit, may be\n     *         {@code null} if not known\n     */\n    public AcmeRateLimitedException(Problem problem, @Nullable Instant retryAfter,\n                @Nullable Collection<URL> documents) {\n        super(problem);\n        this.retryAfter = retryAfter;\n        this.documents = documents != null ? documents : Collections.emptyList();\n    }\n\n    /**\n     * Returns the instant of time the request is expected to succeed again. Empty\n     * if this moment is not known.\n     */\n    public Optional<Instant> getRetryAfter() {\n        return Optional.ofNullable(retryAfter);\n    }\n\n    /**\n     * Collection of URLs pointing to documents about the rate limit that was hit.\n     * Empty if the server did not provide such URLs.\n     */\n    public Collection<URL> getDocuments() {\n        return Collections.unmodifiableCollection(documents);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeServerException.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport java.io.Serial;\nimport java.net.URI;\nimport java.util.Objects;\n\nimport org.shredzone.acme4j.Problem;\n\n/**\n * The ACME server returned an error. The exception contains a {@link Problem} document\n * containing the exact cause of the error.\n * <p>\n * For some special cases, subclasses of this exception are thrown, so they can be handled\n * individually.\n */\npublic class AcmeServerException extends AcmeException {\n    @Serial\n    private static final long serialVersionUID = 5971622508467042792L;\n\n    private final Problem problem;\n\n    /**\n     * Creates a new {@link AcmeServerException}.\n     *\n     * @param problem\n     *            {@link Problem} that caused the exception\n     */\n    public AcmeServerException(Problem problem) {\n        super(Objects.requireNonNull(problem).toString());\n        this.problem = problem;\n    }\n\n    /**\n     * Returns the error type.\n     */\n    public URI getType() {\n        return problem.getType();\n    }\n\n    /**\n     * Returns the {@link Problem} that caused the exception\n     */\n    public Problem getProblem() {\n        return problem;\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeUnauthorizedException.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport java.io.Serial;\n\nimport org.shredzone.acme4j.Problem;\n\n/**\n * The client is not authorized to perform the operation. The {@link Problem} document\n * will give further details (e.g. \"client IP is blocked\").\n */\npublic class AcmeUnauthorizedException extends AcmeServerException {\n    @Serial\n    private static final long serialVersionUID = 9064697508262919366L;\n\n    /**\n     * Creates a new {@link AcmeUnauthorizedException}.\n     *\n     * @param problem\n     *            {@link Problem} that caused the exception\n     */\n    public AcmeUnauthorizedException(Problem problem) {\n        super(problem);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeUserActionRequiredException.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport java.io.Serial;\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.util.Optional;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.acme4j.Problem;\n\n/**\n * The user is required to take manual action as indicated.\n * <p>\n * Usually this exception is thrown when the terms of service have changed, and the CA\n * requires an agreement to the new terms before proceeding.\n */\npublic class AcmeUserActionRequiredException extends AcmeServerException {\n    @Serial\n    private static final long serialVersionUID = 7719055447283858352L;\n\n    private final @Nullable URI tosUri;\n\n    /**\n     * Creates a new {@link AcmeUserActionRequiredException}.\n     *\n     * @param problem\n     *         {@link Problem} that caused the exception\n     * @param tosUri\n     *         {@link URI} of the terms-of-service document to accept, may be\n     *         {@code null}\n     */\n    public AcmeUserActionRequiredException(Problem problem, @Nullable URI tosUri) {\n        super(problem);\n        this.tosUri = tosUri;\n    }\n\n    /**\n     * Returns the {@link URI} of the terms-of-service document to accept. Empty\n     * if the server did not provide a link to such a document.\n     */\n    public Optional<URI> getTermsOfServiceUri() {\n        return Optional.ofNullable(tosUri);\n    }\n\n    /**\n     * Returns the {@link URL} of a document that gives instructions on the actions to be\n     * taken by a human.\n     */\n    public URL getInstance() {\n        var instance = getProblem().getInstance()\n                .orElseThrow(() -> new AcmeProtocolException(\"Instance URL required, but missing.\"));\n\n        try {\n            return instance.toURL();\n        } catch (MalformedURLException ex) {\n            throw new AcmeProtocolException(\"Bad instance URL: \" + instance, ex);\n        }\n    }\n\n    @Override\n    public String toString() {\n        return getProblem().getInstance()\n                .map(uri -> \"Please visit \" + uri + \" - details: \" + getProblem())\n                .orElseGet(super::toString);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/exception/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains all exceptions that can be thrown by acme4j.\n * <p>\n * {@link org.shredzone.acme4j.exception.AcmeException} is the root exception, and other\n * exceptions are derived from it.\n * <p>\n * Some methods that do lazy-loading of remote resources may throw a runtime\n * {@link org.shredzone.acme4j.exception.AcmeLazyLoadingException} instead, so the API is\n * not polluted with checked exceptions on every getter.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.exception;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * acme4j is a Java client for the ACME protocol.\n * <p>\n * See the documentation and the example for how to use this client in your own projects.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeProvider.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.time.ZonedDateTime;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.ServiceLoader;\n\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.challenge.Challenge;\nimport org.shredzone.acme4j.challenge.Dns01Challenge;\nimport org.shredzone.acme4j.challenge.DnsAccount01Challenge;\nimport org.shredzone.acme4j.challenge.DnsPersist01Challenge;\nimport org.shredzone.acme4j.challenge.Http01Challenge;\nimport org.shredzone.acme4j.challenge.TlsAlpn01Challenge;\nimport org.shredzone.acme4j.challenge.TokenChallenge;\nimport org.shredzone.acme4j.connector.Connection;\nimport org.shredzone.acme4j.connector.DefaultConnection;\nimport org.shredzone.acme4j.connector.HttpConnector;\nimport org.shredzone.acme4j.connector.NetworkSettings;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * Abstract implementation of {@link AcmeProvider}. It consists of a challenge\n * registry and a standard {@link HttpConnector}.\n * <p>\n * Implementing classes must implement at least {@link AcmeProvider#accepts(URI)}\n * and {@link AbstractAcmeProvider#resolve(URI)}.\n */\npublic abstract class AbstractAcmeProvider implements AcmeProvider {\n    private static final int HTTP_NOT_MODIFIED = 304;\n\n    private static final Map<String, ChallengeProvider> CHALLENGES = challengeMap();\n\n    @Override\n    public Connection connect(URI serverUri, NetworkSettings networkSettings, HttpClient httpClient) {\n        return new DefaultConnection(createHttpConnector(networkSettings, httpClient));\n    }\n\n    @Override\n    public JSON directory(Session session, URI serverUri) throws AcmeException {\n        var expires = session.getDirectoryExpires();\n        if (expires != null && expires.isAfter(ZonedDateTime.now())) {\n            // The cached directory is still valid\n            return null;\n        }\n\n        try (var nonceHolder = session.lockNonce();\n             var conn = connect(serverUri, session.networkSettings(), session.getHttpClient())) {\n            var lastModified = session.getDirectoryLastModified();\n            var rc = conn.sendRequest(resolve(serverUri), session, lastModified);\n            if (lastModified != null && rc == HTTP_NOT_MODIFIED) {\n                // The server has not been modified since\n                return null;\n            }\n\n            // evaluate caching headers\n            session.setDirectoryLastModified(conn.getLastModified().orElse(null));\n            session.setDirectoryExpires(conn.getExpiration().orElse(null));\n\n            // use nonce header if there is one, saves a HEAD request...\n            conn.getNonce().ifPresent(nonceHolder::setNonce);\n\n            return conn.readJsonResponse();\n        }\n    }\n\n    private static Map<String, ChallengeProvider> challengeMap() {\n        var map = new HashMap<String, ChallengeProvider>();\n\n        map.put(Dns01Challenge.TYPE, Dns01Challenge::new);\n        map.put(DnsAccount01Challenge.TYPE, DnsAccount01Challenge::new);\n        map.put(DnsPersist01Challenge.TYPE, DnsPersist01Challenge::new);\n        map.put(Http01Challenge.TYPE, Http01Challenge::new);\n        map.put(TlsAlpn01Challenge.TYPE, TlsAlpn01Challenge::new);\n\n        for (var provider : ServiceLoader.load(ChallengeProvider.class)) {\n            var typeAnno = provider.getClass().getAnnotation(ChallengeType.class);\n            if (typeAnno == null) {\n                throw new IllegalStateException(\"ChallengeProvider \"\n                        + provider.getClass().getName()\n                        + \" has no @ChallengeType annotation\");\n            }\n            var type = typeAnno.value();\n            if (type == null || type.trim().isEmpty()) {\n                throw new IllegalStateException(\"ChallengeProvider \"\n                        + provider.getClass().getName()\n                        + \": type must not be null or empty\");\n            }\n            if (map.containsKey(type)) {\n                throw new IllegalStateException(\"ChallengeProvider \"\n                        + provider.getClass().getName()\n                        + \": there is already a provider for challenge type \"\n                        + type);\n            }\n            map.put(type, provider);\n        }\n\n        return Collections.unmodifiableMap(map);\n    }\n\n    /**\n     * {@inheritDoc}\n     * <p>\n     * This implementation handles the standard challenge types. For unknown types,\n     * generic {@link Challenge} or {@link TokenChallenge} instances are created.\n     * <p>\n     * Custom provider implementations may override this method to provide challenges that\n     * are proprietary to the provider.\n     */\n    @Override\n    public Challenge createChallenge(Login login, JSON data) {\n        Objects.requireNonNull(login, \"login\");\n        Objects.requireNonNull(data, \"data\");\n\n        var type = data.get(\"type\").asString();\n\n        var constructor = CHALLENGES.get(type);\n        if (constructor != null) {\n            return constructor.create(login, data);\n        }\n\n        if (data.contains(\"token\")) {\n            return new TokenChallenge(login, data);\n        } else {\n            return new Challenge(login, data);\n        }\n    }\n\n    /**\n     * Creates a {@link HttpConnector} with the given {@link NetworkSettings} and\n     * {@link HttpClient}.\n     * <p>\n     * Subclasses may override this method to configure the {@link HttpConnector}.\n     *\n     * @param settings The network settings to use\n     * @param httpClient The HTTP client to use\n     * @return A new {@link HttpConnector} instance\n     * @since 4.0.0\n     */\n    protected HttpConnector createHttpConnector(NetworkSettings settings, HttpClient httpClient) {\n        return new HttpConnector(settings, httpClient);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/AcmeProvider.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider;\n\nimport java.net.URI;\nimport java.net.URL;\nimport java.net.http.HttpClient;\nimport java.util.Optional;\nimport java.util.ServiceLoader;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.challenge.Challenge;\nimport org.shredzone.acme4j.connector.Connection;\nimport org.shredzone.acme4j.connector.NetworkSettings;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * An {@link AcmeProvider} provides methods to be used for communicating with the ACME\n * server. Implementations handle individual features of each ACME server.\n * <p>\n * Provider implementations must be registered with Java's {@link ServiceLoader}.\n */\npublic interface AcmeProvider {\n\n    /**\n     * Checks if this provider accepts the given server URI.\n     *\n     * @param serverUri\n     *            Server URI to test\n     * @return {@code true} if this provider accepts the server URI, {@code false}\n     *         otherwise\n     */\n    boolean accepts(URI serverUri);\n\n    /**\n     * Resolves the server URI and returns the matching directory URL.\n     *\n     * @param serverUri\n     *            Server {@link URI}\n     * @return Resolved directory {@link URL}\n     * @throws IllegalArgumentException\n     *             if the server {@link URI} is not accepted\n     */\n    URL resolve(URI serverUri);\n\n    /**\n     * Creates an {@link HttpClient} instance configured with the given network settings.\n     * <p>\n     * The default implementation creates a standard HttpClient with the network settings.\n     * Subclasses can override this method to create a customized HttpClient, for example\n     * to configure SSL context or other provider-specific requirements.\n     *\n     * @param networkSettings The network settings to use\n     * @return {@link HttpClient} instance\n     * @since 4.0.0\n     */\n    default HttpClient createHttpClient(NetworkSettings networkSettings) {\n        var builder = HttpClient.newBuilder()\n                .followRedirects(HttpClient.Redirect.NORMAL)\n                .connectTimeout(networkSettings.getTimeout())\n                .proxy(networkSettings.getProxySelector());\n\n        if (networkSettings.getAuthenticator() != null) {\n            builder.authenticator(networkSettings.getAuthenticator());\n        }\n\n        return builder.build();\n    }\n\n    /**\n     * Creates a {@link Connection} for communication with the ACME server.\n     *\n     * @param serverUri\n     *         Server {@link URI}\n     * @param networkSettings\n     *         {@link NetworkSettings} to be used for the connection\n     * @param httpClient\n     *         {@link HttpClient} to be used for HTTP requests\n     * @return {@link Connection} that was generated\n     * @since 4.0.0\n     */\n    Connection connect(URI serverUri, NetworkSettings networkSettings, HttpClient httpClient);\n\n    /**\n     * Returns the provider's directory. The structure must contain resource URLs, and may\n     * optionally contain metadata.\n     * <p>\n     * The default implementation resolves the server URI and fetches the directory via\n     * HTTP request. Subclasses may override this method, e.g. if the directory is static.\n     *\n     * @param session\n     *            {@link Session} to be used\n     * @param serverUri\n     *            Server {@link URI}\n     * @return Directory data, as JSON object, or {@code null} if the directory has not\n     * been changed since the last request.\n     */\n    @Nullable\n    JSON directory(Session session, URI serverUri) throws AcmeException;\n\n    /**\n     * Creates a {@link Challenge} instance for the given challenge data.\n     *\n     * @param login\n     *            {@link Login} to bind the challenge to\n     * @param data\n     *            Challenge {@link JSON} data\n     * @return {@link Challenge} instance, or {@code null} if this provider is unable to\n     *         generate a matching {@link Challenge} instance.\n     */\n    @Nullable\n    Challenge createChallenge(Login login, JSON data);\n\n    /**\n     * Returns a proposal for the EAB MAC algorithm to be used. Only set if the CA\n     * requires External Account Binding and the MAC algorithm cannot be correctly derived\n     * from the MAC key. Empty otherwise.\n     *\n     * @return Proposed MAC algorithm to be used for EAB, or empty for the default\n     * behavior.\n     * @since 3.5.0\n     */\n    default Optional<String> getProposedEabMacAlgorithm() {\n        return Optional.empty();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/ChallengeProvider.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider;\n\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.challenge.Challenge;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * A provider that creates a Challenge from a matching JSON.\n *\n * @since 2.12\n */\n@FunctionalInterface\npublic interface ChallengeProvider {\n\n    /**\n     * Creates a Challenge.\n     *\n     * @param login\n     *         {@link Login} of the user's account\n     * @param data\n     *         {@link JSON} of the challenge as sent by the CA\n     * @return Created and initialized {@link Challenge}. It must match the JSON type.\n     */\n    Challenge create(Login login, JSON data);\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/ChallengeType.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Annotates the challenge type that is generated by the {@link ChallengeProvider}.\n *\n * @since 2.12\n */\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.TYPE)\npublic @interface ChallengeType {\n\n    /**\n     * Challenge type.\n     */\n    String value();\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/GenericAcmeProvider.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\n\n/**\n * A generic {@link AcmeProvider}. It should be working for all ACME servers complying to\n * the ACME specifications.\n * <p>\n * The {@code serverUri} is either a http or https URI to the server's directory service.\n */\npublic class GenericAcmeProvider extends AbstractAcmeProvider {\n\n    @Override\n    public boolean accepts(URI serverUri) {\n        return \"http\".equals(serverUri.getScheme())\n                        || \"https\".equals(serverUri.getScheme());\n    }\n\n    @Override\n    public URL resolve(URI serverUri) {\n        try {\n            return serverUri.toURL();\n        } catch (MalformedURLException ex) {\n            throw new IllegalArgumentException(\"Bad generic server URI\", ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/actalis/ActalisAcmeProvider.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2025 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider.actalis;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.util.Map;\n\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.provider.AbstractAcmeProvider;\nimport org.shredzone.acme4j.provider.AcmeProvider;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * An {@link AcmeProvider} for <em>Actalis</em>.\n * <p>\n * The {@code serverUri} is {@code \"acme://actalis.com\"} for the production server.\n * <p>\n * If you want to use <em>Actalis</em>, always prefer to use this provider.\n *\n * @see <a href=\"https://www.actalis.com/\">Actalis S.p.A.</a>\n * @since 4.0.0\n */\npublic class ActalisAcmeProvider extends AbstractAcmeProvider {\n\n    private static final String PRODUCTION_DIRECTORY_URL = \"https://acme-api.actalis.com/acme/directory\";\n\n    @Override\n    public boolean accepts(URI serverUri) {\n        return \"acme\".equals(serverUri.getScheme())\n                && \"actalis.com\".equals(serverUri.getHost());\n    }\n\n    @Override\n    public URL resolve(URI serverUri) {\n        var path = serverUri.getPath();\n        String directoryUrl;\n        if (path == null || path.isEmpty() || \"/\".equals(path)) {\n            directoryUrl = PRODUCTION_DIRECTORY_URL;\n        } else {\n            throw new IllegalArgumentException(\"Unknown URI \" + serverUri);\n        }\n\n        try {\n            return URI.create(directoryUrl).toURL();\n        } catch (MalformedURLException ex) {\n            throw new AcmeProtocolException(directoryUrl, ex);\n        }\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public JSON directory(Session session, URI serverUri) throws AcmeException {\n        // This is a workaround as actalis.com uses \"home\" instead of \"website\" to\n        // refer to its homepage in the metadata.\n        var superdirectory = super.directory(session, serverUri);\n        if (superdirectory == null) {\n            return null;\n        }\n\n        var directory = superdirectory.toMap();\n        var meta = directory.get(\"meta\");\n        if (meta instanceof Map) {\n            var metaMap = ((Map<String, Object>) meta);\n            if (metaMap.containsKey(\"home\") && !metaMap.containsKey(\"website\")) {\n                metaMap.put(\"website\", metaMap.remove(\"home\"));\n            }\n        }\n        return JSON.fromMap(directory);\n    }\n\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/actalis/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2025 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains an {@link org.shredzone.acme4j.provider.AcmeProvider} for the\n * Actalis server.\n *\n * @see <a href=\"https://www.actalis.com/\">Actalis S.p.A.</a>\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.provider.actalis;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/google/GoogleAcmeProvider.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2024 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider.google;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.util.Optional;\n\nimport org.jose4j.jws.AlgorithmIdentifiers;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.provider.AbstractAcmeProvider;\nimport org.shredzone.acme4j.provider.AcmeProvider;\n\n/**\n * An {@link AcmeProvider} for the <em>Google Trust Services</em>.\n * <p>\n * The {@code serverUri} is {@code \"acme://pki.goog\"} for the production server,\n * and {@code \"acme://pki.goog/staging\"} for the staging server.\n *\n * @see <a href=\"https://pki.goog/\">https://pki.goog/</a>\n * @since 3.5.0\n */\npublic class GoogleAcmeProvider extends AbstractAcmeProvider {\n\n    private static final String PRODUCTION_DIRECTORY_URL = \"https://dv.acme-v02.api.pki.goog/directory\";\n    private static final String STAGING_DIRECTORY_URL = \"https://dv.acme-v02.test-api.pki.goog/directory\";\n\n    @Override\n    public boolean accepts(URI serverUri) {\n        return \"acme\".equals(serverUri.getScheme())\n                && \"pki.goog\".equals(serverUri.getHost());\n    }\n\n    @Override\n    public URL resolve(URI serverUri) {\n        var path = serverUri.getPath();\n        String directoryUrl;\n        if (path == null || path.isEmpty() || \"/\".equals(path)) {\n            directoryUrl = PRODUCTION_DIRECTORY_URL;\n        } else if (\"/staging\".equals(path)) {\n            directoryUrl = STAGING_DIRECTORY_URL;\n        } else {\n            throw new IllegalArgumentException(\"Unknown URI \" + serverUri);\n        }\n\n        try {\n            return URI.create(directoryUrl).toURL();\n        } catch (MalformedURLException ex) {\n            throw new AcmeProtocolException(directoryUrl, ex);\n        }\n    }\n\n    @Override\n    public Optional<String> getProposedEabMacAlgorithm() {\n        return Optional.of(AlgorithmIdentifiers.HMAC_SHA256);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/google/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2024 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains the {@link org.shredzone.acme4j.provider.AcmeProvider} for the\n * Google Trust Services.\n *\n * @see <a href=\"https://pki.goog/\">https://pki.goog/</a>\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.provider.google;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/letsencrypt/LetsEncryptAcmeProvider.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider.letsencrypt;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\n\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.provider.AbstractAcmeProvider;\nimport org.shredzone.acme4j.provider.AcmeProvider;\n\n/**\n * An {@link AcmeProvider} for <em>Let's Encrypt</em>.\n * <p>\n * The {@code serverUri} is {@code \"acme://letsencrypt.org\"} for the production server,\n * and {@code \"acme://letsencrypt.org/staging\"} for a testing server.\n * <p>\n * If you want to use <em>Let's Encrypt</em>, always prefer to use this provider.\n *\n * @see <a href=\"https://letsencrypt.org/\">Let's Encrypt</a>\n */\npublic class LetsEncryptAcmeProvider extends AbstractAcmeProvider {\n\n    private static final String V02_DIRECTORY_URL = \"https://acme-v02.api.letsencrypt.org/directory\";\n    private static final String STAGING_DIRECTORY_URL = \"https://acme-staging-v02.api.letsencrypt.org/directory\";\n\n    @Override\n    public boolean accepts(URI serverUri) {\n        return \"acme\".equals(serverUri.getScheme())\n                && \"letsencrypt.org\".equals(serverUri.getHost());\n    }\n\n    @Override\n    public URL resolve(URI serverUri) {\n        var path = serverUri.getPath();\n        String directoryUrl;\n        if (path == null || path.isEmpty() || \"/\".equals(path) || \"/v02\".equals(path)) {\n            directoryUrl = V02_DIRECTORY_URL;\n        } else if (\"/staging\".equals(path)) {\n            directoryUrl = STAGING_DIRECTORY_URL;\n        } else {\n            throw new IllegalArgumentException(\"Unknown URI \" + serverUri);\n        }\n\n        try {\n            return URI.create(directoryUrl).toURL();\n        } catch (MalformedURLException ex) {\n            throw new AcmeProtocolException(directoryUrl, ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/letsencrypt/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains the Let's Encrypt\n * {@link org.shredzone.acme4j.provider.AcmeProvider}.\n *\n * @see <a href=\"https://letsencrypt.org/\">Let's Encrypt</a>\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.provider.letsencrypt;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * Acme Providers are the link between acme4j and the ACME server. They know how to\n * connect to their server, and how to set up HTTP connections.\n * <p>\n * {@link org.shredzone.acme4j.provider.AcmeProvider} is the root interface.\n * {@link org.shredzone.acme4j.provider.AbstractAcmeProvider} is an abstract\n * implementation of the most elementary methods. Most HTTP based providers will extend\n * from {@link org.shredzone.acme4j.provider.GenericAcmeProvider} though.\n * <p>\n * Provider implementations must be registered with Java's\n * {@link java.util.ServiceLoader}.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.provider;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProvider.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider.pebble;\n\nimport java.io.IOException;\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.net.http.HttpClient;\nimport java.security.KeyManagementException;\nimport java.security.KeyStore;\nimport java.security.KeyStoreException;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.cert.CertificateException;\nimport java.security.cert.CertificateFactory;\nimport java.util.Optional;\nimport java.util.regex.Pattern;\n\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.TrustManagerFactory;\n\nimport org.shredzone.acme4j.connector.NetworkSettings;\nimport org.shredzone.acme4j.provider.AbstractAcmeProvider;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * An {@link AcmeProvider} for <em>Pebble</em>.\n * <p>\n * <a href=\"https://github.com/letsencrypt/pebble\">Pebble</a> is a small ACME test server.\n * This provider can be used to connect to an instance of a Pebble server.\n * <p>\n * {@code \"acme://pebble\"} connects to a Pebble server running on localhost and listening\n * on the standard port 14000. Using {@code \"acme://pebble/other-host:12345\"}, it is\n * possible to connect to an external Pebble server on the given {@code other-host} and\n * port. The port is optional, and if omitted, the standard port is used.\n */\npublic class PebbleAcmeProvider extends AbstractAcmeProvider {\n    private static final Logger LOG = LoggerFactory.getLogger(PebbleAcmeProvider.class);\n    private static final Pattern HOST_PATTERN = Pattern.compile(\"^/([^:/]+)(?:\\\\:(\\\\d+))?/?$\");\n    private static final int PEBBLE_DEFAULT_PORT = 14000;\n\n    @Override\n    public boolean accepts(URI serverUri) {\n        return \"acme\".equals(serverUri.getScheme()) && \"pebble\".equals(serverUri.getHost());\n    }\n\n    @Override\n    public URL resolve(URI serverUri) {\n        try {\n            var path = serverUri.getPath();\n            int port = serverUri.getPort() != -1 ? serverUri.getPort() : PEBBLE_DEFAULT_PORT;\n\n            var baseUrl = URI.create(\"https://localhost:\" + port + \"/dir\").toURL();\n\n            if (path != null && !path.isEmpty() && !\"/\".equals(path)) {\n                baseUrl = parsePath(path);\n            }\n\n            return baseUrl;\n        } catch (MalformedURLException ex) {\n            throw new IllegalArgumentException(\"Bad server URI \" + serverUri, ex);\n        }\n    }\n\n    /**\n     * Parses the server URI path and returns the server's base URL.\n     *\n     * @param path\n     *            server URI path\n     * @return URL of the server's base\n     */\n    private URL parsePath(String path) throws MalformedURLException {\n        var m = HOST_PATTERN.matcher(path);\n        if (m.matches()) {\n            var host = m.group(1);\n            var port = PEBBLE_DEFAULT_PORT;\n            if (m.group(2) != null) {\n                port = Integer.parseInt(m.group(2));\n            }\n            try {\n                return new URI(\"https\", null, host, port, \"/dir\", null, null).toURL();\n            } catch (URISyntaxException ex) {\n                throw new IllegalArgumentException(\"Malformed Pebble host/port: \" + path);\n            }\n        } else {\n            throw new IllegalArgumentException(\"Invalid Pebble host/port: \" + path);\n        }\n    }\n\n    @Override\n    public HttpClient createHttpClient(NetworkSettings networkSettings) {\n        var builder = HttpClient.newBuilder()\n                .followRedirects(HttpClient.Redirect.NORMAL)\n                .connectTimeout(networkSettings.getTimeout())\n                .proxy(networkSettings.getProxySelector())\n                .sslContext(createPebbleSSLContext());\n\n        if (networkSettings.getAuthenticator() != null) {\n            builder.authenticator(networkSettings.getAuthenticator());\n        }\n\n        return builder.build();\n    }\n\n    /**\n     * Creates a TrustManagerFactory configured with the Pebble root certificate.\n     * <p>\n     * This method loads the Pebble root certificate from the PEM file and creates\n     * a TrustManagerFactory that trusts certificates signed by Pebble's CA.\n     *\n     * @return TrustManagerFactory configured for Pebble\n     * @throws RuntimeException if the Pebble certificate cannot be found or loaded\n     * @since 4.0.0\n     */\n    protected TrustManagerFactory createPebbleTrustManagerFactory() {\n        try {\n            var keystore = readPemFile(\"/pebble.minica.pem\")\n                    .or(() -> readPemFile(\"/META-INF/pebble.minica.pem\"))\n                    .or(() -> readPemFile(\"/org/shredzone/acme4j/provider/pebble/pebble.minica.pem\"))\n                    .orElseThrow(() -> new RuntimeException(\"Could not find a Pebble root certificate\"));\n\n            var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());\n            tmf.init(keystore);\n            return tmf;\n        } catch (KeyStoreException | NoSuchAlgorithmException ex) {\n            throw new RuntimeException(\"Could not create truststore\", ex);\n        }\n    }\n\n    /**\n     * Creates the Pebble SSL context.\n     * <p>\n     * Since the HTTP client is cached at the session level, this method is only called\n     * once per session, so no additional caching is needed.\n     *\n     * @return SSLContext configured for Pebble\n     */\n    private SSLContext createPebbleSSLContext() {\n        try {\n            var tmf = createPebbleTrustManagerFactory();\n\n            var sslContext = SSLContext.getInstance(\"TLS\");\n            sslContext.init(null, tmf.getTrustManagers(), null);\n            return sslContext;\n        } catch (NoSuchAlgorithmException | KeyManagementException ex) {\n            throw new RuntimeException(\"Could not create SSL context\", ex);\n        }\n    }\n\n    /**\n     * Reads a PEM file from a resource for Pebble SSL context creation.\n     */\n    private Optional<KeyStore> readPemFile(String resource) {\n        try (var in = PebbleAcmeProvider.class.getResourceAsStream(resource)) {\n            if (in == null) {\n                return Optional.empty();\n            }\n            var cf = CertificateFactory.getInstance(\"X.509\");\n            var cert = cf.generateCertificate(in);\n            var keystore = KeyStore.getInstance(KeyStore.getDefaultType());\n            keystore.load(null, \"acme4j\".toCharArray());\n            keystore.setCertificateEntry(\"pebble\", cert);\n            return Optional.of(keystore);\n        } catch (IOException | KeyStoreException | CertificateException\n                 | NoSuchAlgorithmException ex) {\n            LOG.error(\"Failed to read PEM from resource '{}'\", resource, ex);\n            return Optional.empty();\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains an {@link org.shredzone.acme4j.provider.AcmeProvider} for the\n * Pebble test server.\n *\n * @see <a href=\"https://github.com/letsencrypt/pebble\">Pebble project page</a>\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.provider.pebble;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/sslcom/SslComAcmeProvider.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2024 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider.sslcom;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.util.Map;\n\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.provider.AbstractAcmeProvider;\nimport org.shredzone.acme4j.provider.AcmeProvider;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * An {@link AcmeProvider} for <em>SSL.com</em>.\n * <p>\n * The {@code serverUri} is {@code \"acme://ssl.com\"} for the production server,\n * and {@code \"acme://acme-try.ssl.com\"} for a testing server.\n * <p>\n * If you want to use <em>SSL.com</em>, always prefer to use this provider.\n *\n * @see <a href=\"https://ssl.com/\">SSL.com</a>\n * @since 3.2.0\n */\npublic class SslComAcmeProvider extends AbstractAcmeProvider {\n\n    private static final String PRODUCTION_ECC_DIRECTORY_URL = \"https://acme.ssl.com/sslcom-dv-ecc\";\n    private static final String PRODUCTION_RSA_DIRECTORY_URL = \"https://acme.ssl.com/sslcom-dv-rsa\";\n    private static final String STAGING_ECC_DIRECTORY_URL = \"https://acme-try.ssl.com/sslcom-dv-ecc\";\n    private static final String STAGING_RSA_DIRECTORY_URL = \"https://acme-try.ssl.com/sslcom-dv-rsa\";\n\n    @Override\n    public boolean accepts(URI serverUri) {\n        return \"acme\".equals(serverUri.getScheme())\n                && \"ssl.com\".equals(serverUri.getHost());\n    }\n\n    @Override\n    public URL resolve(URI serverUri) {\n        var path = serverUri.getPath();\n        String directoryUrl;\n        if (path == null || path.isEmpty() || \"/\".equals(path) || \"/ecc\".equals(path)) {\n            directoryUrl = PRODUCTION_ECC_DIRECTORY_URL;\n        } else if (\"/rsa\".equals(path)) {\n            directoryUrl = PRODUCTION_RSA_DIRECTORY_URL;\n        } else if (\"/staging\".equals(path) || \"/staging/ecc\".equals(path)) {\n            directoryUrl = STAGING_ECC_DIRECTORY_URL;\n        } else if (\"/staging/rsa\".equals(path)) {\n            directoryUrl = STAGING_RSA_DIRECTORY_URL;\n        } else {\n            throw new IllegalArgumentException(\"Unknown URI \" + serverUri);\n        }\n\n        try {\n            return URI.create(directoryUrl).toURL();\n        } catch (MalformedURLException ex) {\n            throw new AcmeProtocolException(directoryUrl, ex);\n        }\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public JSON directory(Session session, URI serverUri) throws AcmeException {\n        // This is a workaround for a bug at SSL.com. It requires account registration\n        // by EAB, but the \"externalAccountRequired\" flag in the directory is set to\n        // false. This patch reads the directory and forcefully sets the flag to true.\n        // The entire method can be removed once it is fixed on SSL.com side.\n        var superdirectory = super.directory(session, serverUri);\n        if (superdirectory == null) {\n            return null;\n        }\n\n        var directory = superdirectory.toMap();\n        var meta = directory.get(\"meta\");\n        if (meta instanceof Map) {\n            var metaMap = ((Map<String, Object>) meta);\n            metaMap.remove(\"externalAccountRequired\");\n            metaMap.put(\"externalAccountRequired\", true);\n        }\n        return JSON.fromMap(directory);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/sslcom/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains the SSL.com\n * {@link org.shredzone.acme4j.provider.AcmeProvider}.\n *\n * @see <a href=\"https://ssl.com/\">SSL.com</a>\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.provider.sslcom;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/zerossl/ZeroSSLAcmeProvider.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2024 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider.zerossl;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\n\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.provider.AbstractAcmeProvider;\nimport org.shredzone.acme4j.provider.AcmeProvider;\n\n/**\n * An {@link AcmeProvider} for <em>ZeroSSL</em>.\n * <p>\n * The {@code serverUri} is {@code \"acme://zerossl.com\"} for the production server.\n *\n * @see <a href=\"https://zerossl.com/\">ZeroSSL</a>\n * @since 3.2.0\n */\npublic class ZeroSSLAcmeProvider extends AbstractAcmeProvider {\n\n    private static final String V02_DIRECTORY_URL = \"https://acme.zerossl.com/v2/DV90\";\n\n    @Override\n    public boolean accepts(URI serverUri) {\n        return \"acme\".equals(serverUri.getScheme())\n                && \"zerossl.com\".equals(serverUri.getHost());\n    }\n\n    @Override\n    public URL resolve(URI serverUri) {\n        var path = serverUri.getPath();\n        String directoryUrl;\n        if (path == null || path.isEmpty() || \"/\".equals(path)) {\n            directoryUrl = V02_DIRECTORY_URL;\n        } else {\n            throw new IllegalArgumentException(\"Unknown URI \" + serverUri);\n        }\n\n        try {\n            return URI.create(directoryUrl).toURL();\n        } catch (MalformedURLException ex) {\n            throw new AcmeProtocolException(directoryUrl, ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/provider/zerossl/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2024 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains the ZeroSSL {@link org.shredzone.acme4j.provider.AcmeProvider}.\n *\n * @see <a href=\"https://zerossl.com/\">ZeroSSL</a>\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.provider.zerossl;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.toolbox;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.io.Writer;\nimport java.net.IDN;\nimport java.net.URI;\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.cert.X509Certificate;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.util.Arrays;\nimport java.util.Base64;\nimport java.util.Locale;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.regex.Pattern;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.bouncycastle.asn1.ASN1Integer;\nimport org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;\nimport org.bouncycastle.asn1.x509.Certificate;\nimport org.bouncycastle.cert.X509CertificateHolder;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\n\n/**\n * Contains utility methods that are frequently used for the ACME protocol.\n * <p>\n * This class is internal. You may use it in your own code, but be warned that methods may\n * change their signature or disappear without prior announcement.\n */\npublic final class AcmeUtils {\n    private static final char[] HEX = \"0123456789abcdef\".toCharArray();\n    private static final String ACME_ERROR_PREFIX = \"urn:ietf:params:acme:error:\";\n\n    private static final Pattern DATE_PATTERN = Pattern.compile(\n                    \"^(\\\\d{4})-(\\\\d{2})-(\\\\d{2})T\"\n                  + \"(\\\\d{2}):(\\\\d{2}):(\\\\d{2})\"\n                  + \"(?:\\\\.(\\\\d{1,3})\\\\d*)?\"\n                  + \"(Z|[+-]\\\\d{2}:?\\\\d{2})$\", Pattern.CASE_INSENSITIVE);\n\n    private static final Pattern TZ_PATTERN = Pattern.compile(\n                \"([+-])(\\\\d{2}):?(\\\\d{2})$\");\n\n    private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(\n                \"([^;]+)(?:;.*?charset=(\\\"?)([a-z0-9_-]+)(\\\\2))?.*\", Pattern.CASE_INSENSITIVE);\n\n    private static final Pattern MAIL_PATTERN = Pattern.compile(\"\\\\?|@.*,\");\n\n    private static final Pattern BASE64URL_PATTERN = Pattern.compile(\"[0-9A-Za-z_-]*\");\n\n    private static final Base64.Encoder PEM_ENCODER = Base64.getMimeEncoder(64,\n                \"\\n\".getBytes(StandardCharsets.US_ASCII));\n    private static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder().withoutPadding();\n    private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder();\n\n    private static final char[] BASE32_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\".toCharArray();\n\n    /**\n     * Enumeration of PEM labels.\n     */\n    public enum PemLabel {\n        CERTIFICATE(\"CERTIFICATE\"),\n        CERTIFICATE_REQUEST(\"CERTIFICATE REQUEST\"),\n        PRIVATE_KEY(\"PRIVATE KEY\"),\n        PUBLIC_KEY(\"PUBLIC KEY\");\n\n        private final String label;\n\n        PemLabel(String label) {\n            this.label = label;\n        }\n\n        @Override\n        public String toString() {\n            return label;\n        }\n    }\n\n\n    private AcmeUtils() {\n        // Utility class without constructor\n    }\n\n    /**\n     * Computes a SHA-256 hash of the given string.\n     *\n     * @param z\n     *            String to hash\n     * @return Hash\n     */\n    public static byte[] sha256hash(String z) {\n        try {\n            var md = MessageDigest.getInstance(\"SHA-256\");\n            md.update(z.getBytes(UTF_8));\n            return md.digest();\n        } catch (NoSuchAlgorithmException ex) {\n            throw new AcmeProtocolException(\"Could not compute hash\", ex);\n        }\n    }\n\n    /**\n     * Hex encodes the given byte array.\n     *\n     * @param data\n     *            byte array to hex encode\n     * @return Hex encoded string of the data (with lower case characters)\n     */\n    public static String hexEncode(byte[] data) {\n        var result = new char[data.length * 2];\n        for (var ix = 0; ix < data.length; ix++) {\n            var val = data[ix] & 0xFF;\n            result[ix * 2] = HEX[val >>> 4];\n            result[ix * 2 + 1] = HEX[val & 0x0F];\n        }\n        return new String(result);\n    }\n\n    /**\n     * Base64 encodes the given byte array, using URL style encoding.\n     *\n     * @param data\n     *            byte array to base64 encode\n     * @return base64 encoded string\n     */\n    public static String base64UrlEncode(byte[] data) {\n        return URL_ENCODER.encodeToString(data);\n    }\n\n    /**\n     * Base64 decodes to a byte array, using URL style encoding.\n     *\n     * @param base64\n     *            base64 encoded string\n     * @return decoded data\n     */\n    public static byte[] base64UrlDecode(String base64) {\n        return URL_DECODER.decode(base64);\n    }\n\n    /**\n     * Base32 encodes a byte array.\n     *\n     * @param data Byte array to encode\n     * @return Base32 encoded data (includes padding)\n     * @since 4.0.0\n     */\n    public static String base32Encode(byte[] data) {\n        var result = new StringBuilder();\n        var unconverted = new int[5];\n        var converted = new int[8];\n\n        for (var ix = 0; ix < (data.length + 4) / 5; ix++) {\n            var blocklen = unconverted.length;\n            for (var pos = 0; pos < unconverted.length; pos++) {\n                if ((ix * 5 + pos) < data.length) {\n                    unconverted[pos] = data[ix * 5 + pos] & 0xFF;\n                } else {\n                    unconverted[pos] = 0;\n                    blocklen--;\n                }\n            }\n\n            converted[0] = (unconverted[0] >> 3) & 0x1F;\n            converted[1] = ((unconverted[0] & 0x07) << 2) | ((unconverted[1] >> 6) & 0x03);\n            converted[2] = (unconverted[1] >> 1) & 0x1F;\n            converted[3] = ((unconverted[1] & 0x01) << 4) | ((unconverted[2] >> 4) & 0x0F);\n            converted[4] = ((unconverted[2] & 0x0F) << 1) | ((unconverted[3] >> 7) & 0x01);\n            converted[5] = (unconverted[3] >> 2) & 0x1F;\n            converted[6] = ((unconverted[3] & 0x03) << 3) | ((unconverted[4] >> 5) & 0x07);\n            converted[7] = unconverted[4] & 0x1F;\n\n            var padding = switch (blocklen) {\n                case 1 -> 6;\n                case 2 -> 4;\n                case 3 -> 3;\n                case 4 -> 1;\n                case 5 -> 0;\n                default -> throw new IllegalArgumentException(\"blocklen \" + blocklen + \" out of range\");\n            };\n\n            Arrays.stream(converted)\n                    .limit(converted.length - padding)\n                    .map(v -> BASE32_ALPHABET[v])\n                    .forEach(v -> result.append((char) v));\n\n            if (padding > 0) {\n                result.append(\"=\".repeat(padding));\n            }\n        }\n        return result.toString();\n    }\n\n    /**\n     * Validates that the given {@link String} is a valid base64url encoded value.\n     *\n     * @param base64\n     *            {@link String} to validate\n     * @return {@code true}: String contains a valid base64url encoded value.\n     *         {@code false} if the {@link String} was {@code null} or contained illegal\n     *         characters.\n     * @since 2.6\n     */\n    public static boolean isValidBase64Url(@Nullable String base64) {\n        return base64 != null && BASE64URL_PATTERN.matcher(base64).matches();\n    }\n\n    /**\n     * ASCII encodes a domain name.\n     * <p>\n     * The conversion is done as described in\n     * <a href=\"http://www.ietf.org/rfc/rfc3490.txt\">RFC 3490</a>. Additionally, all\n     * leading and trailing white spaces are trimmed, and the result is lowercased.\n     * <p>\n     * It is safe to pass in ACE encoded domains, they will be returned unchanged.\n     *\n     * @param domain\n     *            Domain name to encode\n     * @return Encoded domain name, white space trimmed and lower cased.\n     */\n    public static String toAce(String domain) {\n        Objects.requireNonNull(domain, \"domain\");\n        return IDN.toASCII(domain.trim()).toLowerCase(Locale.ENGLISH);\n    }\n\n    /**\n     * Parses a RFC 3339 formatted date.\n     *\n     * @param str\n     *            Date string\n     * @return {@link Instant} that was parsed\n     * @throws IllegalArgumentException\n     *             if the date string was not RFC 3339 formatted\n     * @see <a href=\"https://www.ietf.org/rfc/rfc3339.txt\">RFC 3339</a>\n     */\n    public static Instant parseTimestamp(String str) {\n        var m = DATE_PATTERN.matcher(str);\n        if (!m.matches()) {\n            throw new IllegalArgumentException(\"Illegal date: \" + str);\n        }\n\n        var year = Integer.parseInt(m.group(1));\n        var month = Integer.parseInt(m.group(2));\n        var dom = Integer.parseInt(m.group(3));\n        var hour = Integer.parseInt(m.group(4));\n        var minute = Integer.parseInt(m.group(5));\n        var second = Integer.parseInt(m.group(6));\n\n        var msStr = new StringBuilder();\n        if (m.group(7) != null) {\n            msStr.append(m.group(7));\n        }\n        while (msStr.length() < 3) {\n            msStr.append('0');\n        }\n        var ms = Integer.parseInt(msStr.toString());\n\n        var tz = m.group(8);\n        if (\"Z\".equalsIgnoreCase(tz)) {\n            tz = \"GMT\";\n        } else {\n            tz = TZ_PATTERN.matcher(tz).replaceAll(\"GMT$1$2:$3\");\n        }\n\n        return ZonedDateTime.of(\n                year, month, dom, hour, minute, second, ms * 1_000_000,\n                ZoneId.of(tz)).toInstant();\n    }\n\n    /**\n     * Converts the given locale to an Accept-Language header value.\n     *\n     * @param locale\n     *         {@link Locale} to be used in the header\n     * @return Value that can be used in an Accept-Language header\n     */\n    public static String localeToLanguageHeader(@Nullable Locale locale) {\n        if (locale == null || \"und\".equals(locale.toLanguageTag())) {\n            return \"*\";\n        }\n\n        var langTag = locale.toLanguageTag();\n\n        var header = new StringBuilder(langTag);\n        if (langTag.indexOf('-') >= 0) {\n            header.append(',').append(locale.getLanguage()).append(\";q=0.8\");\n        }\n        header.append(\",*;q=0.1\");\n\n        return header.toString();\n    }\n\n    /**\n     * Strips the acme error prefix from the error string.\n     * <p>\n     * For example, for \"urn:ietf:params:acme:error:unauthorized\", \"unauthorized\" is\n     * returned.\n     *\n     * @param type\n     *            Error type to strip the prefix from. {@code null} is safe.\n     * @return Stripped error type, or {@code null} if the prefix was not found.\n     */\n    @Nullable\n    public static String stripErrorPrefix(@Nullable String type) {\n        if (type != null && type.startsWith(ACME_ERROR_PREFIX)) {\n            return type.substring(ACME_ERROR_PREFIX.length());\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * Writes an encoded key or certificate to a file in PEM format.\n     *\n     * @param encoded\n     *            Encoded data to write\n     * @param label\n     *            {@link PemLabel} to be used\n     * @param out\n     *            {@link Writer} to write to. It will not be closed after use!\n     */\n    public static void writeToPem(byte[] encoded, PemLabel label, Writer out)\n                throws IOException {\n        out.append(\"-----BEGIN \").append(label.toString()).append(\"-----\\n\");\n        out.append(new String(PEM_ENCODER.encode(encoded), StandardCharsets.US_ASCII));\n        out.append(\"\\n-----END \").append(label.toString()).append(\"-----\\n\");\n    }\n\n    /**\n     * Extracts the content type of a Content-Type header.\n     *\n     * @param header\n     *            Content-Type header\n     * @return Content-Type, or {@code null} if the header was invalid or empty\n     * @throws AcmeProtocolException\n     *             if the Content-Type header contains a different charset than \"utf-8\".\n     */\n    @Nullable\n    public static String getContentType(@Nullable String header) {\n        if (header != null) {\n            var m = CONTENT_TYPE_PATTERN.matcher(header);\n            if (m.matches()) {\n                var charset = m.group(3);\n                if (charset != null && !\"utf-8\".equalsIgnoreCase(charset)) {\n                    throw new AcmeProtocolException(\"Unsupported charset \" + charset);\n                }\n                return m.group(1).trim().toLowerCase(Locale.ENGLISH);\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Validates a contact {@link URI}.\n     *\n     * @param contact\n     *            Contact {@link URI} to validate\n     * @throws IllegalArgumentException\n     *             if the contact {@link URI} is not suitable for account contacts.\n     */\n    public static void validateContact(URI contact) {\n        if (\"mailto\".equalsIgnoreCase(contact.getScheme())) {\n            var address = contact.toString().substring(7);\n            if (MAIL_PATTERN.matcher(address).find()) {\n                throw new IllegalArgumentException(\n                        \"multiple recipients or hfields are not allowed: \" + contact);\n            }\n        }\n    }\n\n    /**\n     * Returns the certificate's unique identifier for renewal.\n     *\n     * @param certificate\n     *         Certificate to get the unique identifier for.\n     * @return Unique identifier\n     * @throws AcmeProtocolException\n     *         if the certificate is invalid or does not provide the necessary\n     *         information.\n     */\n    public static String getRenewalUniqueIdentifier(X509Certificate certificate) {\n        try {\n            var cert = new X509CertificateHolder(certificate.getEncoded());\n\n            var aki = Optional.of(cert)\n                    .map(X509CertificateHolder::getExtensions)\n                    .map(AuthorityKeyIdentifier::fromExtensions)\n                    .map(AuthorityKeyIdentifier::getKeyIdentifier)\n                    .map(AcmeUtils::base64UrlEncode)\n                    .orElseThrow(() -> new AcmeProtocolException(\"Missing or invalid Authority Key Identifier\"));\n\n            var sn = Optional.of(cert)\n                    .map(X509CertificateHolder::toASN1Structure)\n                    .map(Certificate::getSerialNumber)\n                    .map(AcmeUtils::getRawInteger)\n                    .map(AcmeUtils::base64UrlEncode)\n                    .orElseThrow(() -> new AcmeProtocolException(\"Missing or invalid serial number\"));\n\n            return aki + '.' + sn;\n        } catch (Exception ex) {\n            throw new AcmeProtocolException(\"Invalid certificate\", ex);\n        }\n    }\n\n    /**\n     * Gets the raw integer array from ASN1Integer. This is done by encoding it to a byte\n     * array, and then skipping the INTEGER identifier. Other methods of ASN1Integer only\n     * deliver a parsed integer value that might have been mangled.\n     *\n     * @param integer\n     *         ASN1Integer to convert to raw\n     * @return Byte array of the raw integer\n     */\n    private static byte[] getRawInteger(ASN1Integer integer) {\n        try {\n            var encoded = integer.getEncoded();\n            return Arrays.copyOfRange(encoded, 2, encoded.length);\n        } catch (IOException ex) {\n            throw new UncheckedIOException(ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.toolbox;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static java.util.stream.Collectors.joining;\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.NoSuchElementException;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Function;\nimport java.util.stream.Stream;\nimport java.util.stream.StreamSupport;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.jose4j.json.JsonUtil;\nimport org.jose4j.lang.JoseException;\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.Problem;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.exception.AcmeNotSupportedException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\n\n/**\n * A model containing a JSON result. The content is immutable.\n */\npublic final class JSON implements Serializable {\n    @Serial\n    private static final long serialVersionUID = 418332625174149030L;\n\n    private static final JSON EMPTY_JSON = new JSON(new HashMap<>());\n\n    private final String path;\n    private final Map<String, Object> data;\n\n    /**\n     * Creates a new {@link JSON} root object.\n     *\n     * @param data\n     *            {@link Map} containing the parsed JSON data\n     */\n    private JSON(Map<String, Object> data) {\n        this(\"\", data);\n    }\n\n    /**\n     * Creates a new {@link JSON} branch object.\n     *\n     * @param path\n     *            Path leading to this branch.\n     * @param data\n     *            {@link Map} containing the parsed JSON data\n     */\n    private JSON(String path, Map<String, Object> data) {\n        this.path = path;\n        this.data = data;\n    }\n\n    /**\n     * Parses JSON from an {@link InputStream}.\n     *\n     * @param in\n     *            {@link InputStream} to read from. Will be closed after use.\n     * @return {@link JSON} of the read content.\n     */\n    public static JSON parse(InputStream in) throws IOException {\n        try (var reader = new BufferedReader(new InputStreamReader(in, UTF_8))) {\n            var json = reader.lines().map(String::trim).collect(joining());\n            return parse(json);\n        }\n    }\n\n    /**\n     * Parses JSON from a String.\n     *\n     * @param json\n     *            JSON string\n     * @return {@link JSON} of the read content.\n     */\n    public static JSON parse(String json) {\n        try {\n            return new JSON(JsonUtil.parseJson(json));\n        } catch (JoseException ex) {\n            throw new AcmeProtocolException(\"Bad JSON: \" + json, ex);\n        }\n    }\n\n    /**\n     * Creates a JSON object from a map.\n     * <p>\n     * The map's content is deeply copied. Changes to the map won't reflect in the created\n     * JSON structure.\n     *\n     * @param data\n     *         Map structure\n     * @return {@link JSON} of the map's content.\n     * @since 3.2.0\n     */\n    public static JSON fromMap(Map<String, Object> data) {\n        return JSON.parse(JsonUtil.toJson(data));\n    }\n\n    /**\n     * Returns a {@link JSON} of an empty document.\n     *\n     * @return Empty {@link JSON}\n     */\n    public static JSON empty() {\n        return EMPTY_JSON;\n    }\n\n    /**\n     * Returns a set of all keys of this object.\n     *\n     * @return {@link Set} of keys\n     */\n    public Set<String> keySet() {\n        return Collections.unmodifiableSet(data.keySet());\n    }\n\n    /**\n     * Checks if this object contains the given key.\n     *\n     * @param key\n     *            Name of the key to check\n     * @return {@code true} if the key is present\n     */\n    public boolean contains(String key) {\n        return data.containsKey(key);\n    }\n\n    /**\n     * Returns the {@link Value} of the given key.\n     *\n     * @param key\n     *            Key to read\n     * @return {@link Value} of the key\n     */\n    public Value get(String key) {\n        return new Value(\n                path.isEmpty() ? key : path + '.' + key,\n                data.get(key));\n    }\n\n    /**\n     * Returns the {@link Value} of the given key.\n     *\n     * @param key\n     *         Key to read\n     * @return {@link Value} of the key\n     * @throws AcmeNotSupportedException\n     *         if the key is not present. The key is used as feature name.\n     */\n    public Value getFeature(String key) {\n        return new Value(\n                path.isEmpty() ? key : path + '.' + key,\n                data.get(key)).onFeature(key);\n    }\n\n    /**\n     * Returns the content as JSON string.\n     */\n    @Override\n    public String toString() {\n        return JsonUtil.toJson(data);\n    }\n\n    /**\n     * Returns the content as unmodifiable Map.\n     *\n     * @since 2.8\n     */\n    public Map<String,Object> toMap() {\n        return Collections.unmodifiableMap(data);\n    }\n\n    /**\n     * Represents a JSON array.\n     */\n    public static final class Array implements Iterable<Value> {\n        private final String path;\n        private final List<Object> data;\n\n        /**\n         * Creates a new {@link Array} object.\n         *\n         * @param path\n         *            JSON path to this array.\n         * @param data\n         *            Array data\n         */\n        private Array(String path, List<Object> data) {\n            this.path = path;\n            this.data = data;\n        }\n\n        /**\n         * Returns the array size.\n         *\n         * @return Size of the array\n         */\n        public int size() {\n            return data.size();\n        }\n\n        /**\n         * Returns {@code true} if the array is empty.\n         */\n        public boolean isEmpty() {\n            return data.isEmpty();\n        }\n\n        /**\n         * Gets the {@link Value} at the given index.\n         *\n         * @param index\n         *            Array index to read from\n         * @return {@link Value} at this index\n         */\n        public Value get(int index) {\n            return new Value(path + '[' + index + ']', data.get(index));\n        }\n\n        /**\n         * Returns a stream of values.\n         *\n         * @return {@link Stream} of all {@link Value} of this array\n         */\n        public Stream<Value> stream() {\n            return StreamSupport.stream(spliterator(), false);\n        }\n\n        /**\n         * Creates a new {@link Iterator} that iterates over the array {@link Value}.\n         */\n        @Override\n        public Iterator<Value> iterator() {\n            return new ValueIterator(this);\n        }\n    }\n\n    /**\n     * A single JSON value. This instance also covers {@code null} values.\n     * <p>\n     * All return values are never {@code null} unless specified otherwise. For optional\n     * parameters, use {@link Value#optional()}.\n     */\n    public static final class Value {\n        private final String path;\n        private final @Nullable Object val;\n\n        /**\n         * Creates a new {@link Value}.\n         *\n         * @param path\n         *            JSON path to this value\n         * @param val\n         *            Value, may be {@code null}\n         */\n        private Value(String path, @Nullable Object val) {\n            this.path = path;\n            this.val = val;\n        }\n\n        /**\n         * Checks if this value is {@code null}.\n         *\n         * @return {@code true} if this value is present, {@code false} if {@code null}.\n         */\n        public boolean isPresent() {\n            return val != null;\n        }\n\n        /**\n         * Returns this value as {@link Optional}, for further mapping and filtering.\n         *\n         * @return {@link Optional} of this value, or {@link Optional#empty()} if this\n         *         value is {@code null}.\n         * @see #map(Function)\n         */\n        public Optional<Value> optional() {\n            return val != null ? Optional.of(this) : Optional.empty();\n        }\n\n        /**\n         * Returns this value. If the value was {@code null}, an\n         * {@link AcmeNotSupportedException} is thrown. This method is used for mandatory\n         * fields that are only present if a certain feature is supported by the server.\n         *\n         * @param feature\n         *         Feature name\n         * @return itself\n         */\n        public Value onFeature(String feature) {\n            if (val == null) {\n                throw new AcmeNotSupportedException(feature);\n            }\n            return this;\n        }\n\n        /**\n         * Returns this value as an {@link Optional} of the desired type, for further\n         * mapping and filtering.\n         *\n         * @param mapper\n         *            A {@link Function} that converts a {@link Value} to the desired type\n         * @return {@link Optional} of this value, or {@link Optional#empty()} if this\n         *         value is {@code null}.\n         * @see #optional()\n         */\n        public <T> Optional<T> map(Function <Value, T> mapper) {\n            return optional().map(mapper);\n        }\n\n        /**\n         * Returns the value as {@link String}.\n         */\n        public String asString() {\n            return required().toString();\n        }\n\n        /**\n         * Returns the value as JSON object.\n         */\n        @SuppressWarnings(\"unchecked\")\n        public JSON asObject() {\n            return new JSON(path, (Map<String, Object>) required(Map.class));\n        }\n\n        /**\n         * Returns the value as JSON object that was Base64 URL encoded.\n         *\n         * @since 2.8\n         */\n        public JSON asEncodedObject() {\n            try {\n                var raw = AcmeUtils.base64UrlDecode(asString());\n                return new JSON(path, JsonUtil.parseJson(new String(raw, UTF_8)));\n            } catch (IllegalArgumentException | JoseException ex) {\n                throw new AcmeProtocolException(path + \": expected an encoded object\", ex);\n            }\n        }\n\n        /**\n         * Returns the value as {@link Problem}.\n         *\n         * @param baseUrl\n         *            Base {@link URL} to resolve relative links against\n         */\n        public Problem asProblem(URL baseUrl) {\n            return new Problem(asObject(), baseUrl);\n        }\n\n        /**\n         * Returns the value as {@link Identifier}.\n         *\n         * @since 2.3\n         */\n        public Identifier asIdentifier() {\n            return new Identifier(asObject());\n        }\n\n        /**\n         * Returns the value as {@link JSON.Array}.\n         * <p>\n         * Unlike the other getters, this method returns an empty array if the value is\n         * not set. Use {@link #isPresent()} to find out if the value was actually set.\n         */\n        @SuppressWarnings(\"unchecked\")\n        public Array asArray() {\n            if (val == null) {\n                return new Array(path, Collections.emptyList());\n            }\n\n            try {\n                return new Array(path, (List<Object>) val);\n            } catch (ClassCastException ex) {\n                throw new AcmeProtocolException(path + \": expected an array\", ex);\n            }\n        }\n\n        /**\n         * Returns the value as int.\n         */\n        public int asInt() {\n            return (required(Number.class)).intValue();\n        }\n\n        /**\n         * Returns the value as boolean.\n         */\n        public boolean asBoolean() {\n            return required(Boolean.class);\n        }\n\n        /**\n         * Returns the value as {@link URI}.\n         */\n        public URI asURI() {\n            try {\n                return new URI(asString());\n            } catch (URISyntaxException ex) {\n                throw new AcmeProtocolException(path + \": bad URI \" + val, ex);\n            }\n        }\n\n        /**\n         * Returns the value as {@link URL}.\n         */\n        public URL asURL() {\n            try {\n                return asURI().toURL();\n            } catch (MalformedURLException ex) {\n                throw new AcmeProtocolException(path + \": bad URL \" + val, ex);\n            }\n        }\n\n        /**\n         * Returns the value as {@link Instant}.\n         */\n        public Instant asInstant() {\n            try {\n                return parseTimestamp(asString());\n            } catch (IllegalArgumentException ex) {\n                throw new AcmeProtocolException(path + \": bad date \" + val, ex);\n            }\n        }\n\n        /**\n         * Returns the value as {@link Duration}.\n         *\n         * @since 2.3\n         */\n        public Duration asDuration() {\n            return Duration.ofSeconds(required(Number.class).longValue());\n        }\n\n        /**\n         * Returns the value as base64 decoded byte array.\n         */\n        public byte[] asBinary() {\n            return AcmeUtils.base64UrlDecode(asString());\n        }\n\n        /**\n         * Returns the parsed {@link Status}.\n         */\n        public Status asStatus() {\n            return Status.parse(asString());\n        }\n\n        /**\n         * Checks if the value is present. An {@link AcmeProtocolException} is thrown if\n         * the value is {@code null}.\n         *\n         * @return val that is guaranteed to be non-{@code null}\n         */\n        private Object required() {\n            if (val == null) {\n                throw new AcmeProtocolException(path + \": required, but not set\");\n            }\n            return val;\n        }\n\n        /**\n         * Checks if the value is present. An {@link AcmeProtocolException} is thrown if\n         * the value is {@code null} or is not of the expected type.\n         *\n         * @param type\n         *         expected type\n         * @return val that is guaranteed to be non-{@code null}\n         */\n        private <T> T required(Class<T> type) {\n            if (val == null) {\n                throw new AcmeProtocolException(path + \": required, but not set\");\n            }\n            if (!type.isInstance(val)) {\n                throw new AcmeProtocolException(path + \": cannot convert to \" + type.getSimpleName());\n            }\n            return type.cast(val);\n        }\n\n        @Override\n        public boolean equals(Object obj) {\n            if (!(obj instanceof Value)) {\n                return false;\n            }\n            return Objects.equals(val, ((Value) obj).val);\n        }\n\n        @Override\n        public int hashCode() {\n            return val != null ? val.hashCode() : 0;\n        }\n    }\n\n    /**\n     * An {@link Iterator} over array {@link Value}.\n     */\n    private static class ValueIterator implements Iterator<Value> {\n        private final Array array;\n        private int index = 0;\n\n        public ValueIterator(Array array) {\n            this.array = array;\n        }\n\n        @Override\n        public boolean hasNext() {\n            return index < array.size();\n        }\n\n        @Override\n        public Value next() {\n            if (!hasNext()) {\n                throw new NoSuchElementException();\n            }\n            return array.get(index++);\n        }\n\n        @Override\n        public void remove() {\n            throw new UnsupportedOperationException();\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSONBuilder.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.toolbox;\n\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.base64UrlEncode;\n\nimport java.security.Key;\nimport java.security.PublicKey;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Objects;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.jose4j.json.JsonUtil;\n\n/**\n * Builder for JSON structures.\n * <p>\n * Example:\n * <pre>\n * JSONBuilder cb = new JSONBuilder();\n * cb.put(\"foo\", 123).put(\"bar\", \"hello world\");\n * cb.object(\"sub\").put(\"data\", \"subdata\");\n * cb.array(\"array\", 123, 456, 789);\n * </pre>\n */\npublic class JSONBuilder {\n\n    private final Map<String, Object> data = new LinkedHashMap<>();\n\n    /**\n     * Puts a property. If a property with the key exists, it will be replaced.\n     *\n     * @param key\n     *            Property key\n     * @param value\n     *            Property value\n     * @return {@code this}\n     */\n    public JSONBuilder put(String key, @Nullable Object value) {\n        data.put(Objects.requireNonNull(key, \"key\"), value);\n        return this;\n    }\n\n    /**\n     * Puts an {@link Instant} to the JSON. If a property with the key exists, it will be\n     * replaced.\n     *\n     * @param key\n     *            Property key\n     * @param value\n     *            Property {@link Instant} value\n     * @return {@code this}\n     */\n    public JSONBuilder put(String key, @Nullable Instant value) {\n        if (value == null) {\n            put(key, (Object) null);\n            return this;\n        }\n\n        put(key, DateTimeFormatter.ISO_INSTANT.format(value));\n        return this;\n    }\n\n    /**\n     * Puts a {@link Duration} to the JSON. If a property with the key exists, it will be\n     * replaced.\n     *\n     * @param key\n     *            Property key\n     * @param value\n     *            Property {@link Duration} value\n     * @return {@code this}\n     * @since 2.3\n     */\n    public JSONBuilder put(String key, @Nullable Duration value) {\n        if (value == null) {\n            put(key, (Object) null);\n            return this;\n        }\n\n        put(key, value.getSeconds());\n        return this;\n    }\n\n    /**\n     * Puts binary data to the JSON. The data is base64 url encoded.\n     *\n     * @param key\n     *            Property key\n     * @param data\n     *            Property data\n     * @return {@code this}\n     */\n    public JSONBuilder putBase64(String key, byte[] data) {\n        return put(key, base64UrlEncode(data));\n    }\n\n    /**\n     * Puts a {@link Key} into the claim. The key is serializied as JWK.\n     *\n     * @param key\n     *            Property key\n     * @param publickey\n     *            {@link PublicKey} to serialize\n     * @return {@code this}\n     */\n    public JSONBuilder putKey(String key, PublicKey publickey) {\n        Objects.requireNonNull(publickey, \"publickey\");\n\n        data.put(key, JoseUtils.publicKeyToJWK(publickey));\n        return this;\n    }\n\n    /**\n     * Creates an object for the given key.\n     *\n     * @param key\n     *            Key of the object\n     * @return Newly created {@link JSONBuilder} for the object.\n     */\n    public JSONBuilder object(String key) {\n        var subBuilder = new JSONBuilder();\n        data.put(key, subBuilder.data);\n        return subBuilder;\n    }\n\n    /**\n     * Puts an array.\n     *\n     * @param key\n     *            Property key\n     * @param values\n     *            Collection of property values\n     * @return {@code this}\n     */\n    public JSONBuilder array(String key, Collection<?> values) {\n        data.put(key, values);\n        return this;\n    }\n\n    /**\n     * Returns a {@link Map} representation of the current state.\n     *\n     * @return {@link Map} of the current state\n     */\n    public Map<String, Object> toMap() {\n        return Collections.unmodifiableMap(data);\n    }\n\n    /**\n     * Returns a {@link JSON} representation of the current state.\n     *\n     * @return {@link JSON} of the current state\n     */\n    public JSON toJSON() {\n        return JSON.parse(toString());\n    }\n\n    /**\n     * Returns a JSON string representation of the current state.\n     */\n    @Override\n    public String toString() {\n        return JsonUtil.toJson(data);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JoseUtils.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2019 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.toolbox;\n\nimport java.net.URL;\nimport java.security.KeyPair;\nimport java.security.PublicKey;\nimport java.util.Map;\n\nimport javax.crypto.SecretKey;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.jose4j.jwk.EllipticCurveJsonWebKey;\nimport org.jose4j.jwk.JsonWebKey;\nimport org.jose4j.jwk.PublicJsonWebKey;\nimport org.jose4j.jwk.RsaJsonWebKey;\nimport org.jose4j.jws.AlgorithmIdentifiers;\nimport org.jose4j.jws.JsonWebSignature;\nimport org.jose4j.lang.JoseException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Utility class that takes care of all the JOSE stuff.\n * <p>\n * Internal class, do not use in your project! The API may change anytime, in a breaking\n * manner, and without prior notice.\n *\n * @since 2.7\n */\npublic final class JoseUtils {\n\n    private static final Logger LOG = LoggerFactory.getLogger(JoseUtils.class);\n\n    private JoseUtils() {\n        // Utility class without constructor\n    }\n\n    /**\n     * Creates an ACME JOSE request.\n     *\n     * @param url\n     *         {@link URL} of the ACME call\n     * @param keypair\n     *         {@link KeyPair} to sign the request with\n     * @param payload\n     *         ACME JSON payload. If {@code null}, a POST-as-GET request is generated\n     *         instead.\n     * @param nonce\n     *         Nonce to be used. {@code null} if no nonce is to be used in the JOSE\n     *         header.\n     * @param kid\n     *         kid to be used in the JOSE header. If {@code null}, a jwk header of the\n     *         given key is used instead.\n     * @return JSON structure of the JOSE request, ready to be sent.\n     */\n    public static JSONBuilder createJoseRequest(URL url, KeyPair keypair,\n                @Nullable JSONBuilder payload, @Nullable String nonce, @Nullable String kid) {\n        try {\n            var jwk = PublicJsonWebKey.Factory.newPublicJwk(keypair.getPublic());\n\n            var jws = new JsonWebSignature();\n            jws.getHeaders().setObjectHeaderValue(\"url\", url);\n\n            if (kid != null) {\n                jws.getHeaders().setObjectHeaderValue(\"kid\", kid);\n            } else {\n                jws.getHeaders().setJwkHeaderValue(\"jwk\", jwk);\n            }\n\n            if (nonce != null) {\n                jws.getHeaders().setObjectHeaderValue(\"nonce\", nonce);\n            }\n\n            jws.setPayload(payload != null ? payload.toString() : \"\");\n            jws.setAlgorithmHeaderValue(keyAlgorithm(jwk));\n            jws.setKey(keypair.getPrivate());\n            jws.sign();\n\n            if (LOG.isDebugEnabled()) {\n                LOG.debug(\"{} {}\", payload != null ? \"POST\" : \"POST-as-GET\", url);\n                if (payload != null) {\n                    LOG.debug(\"  Payload: {}\", payload);\n                }\n                LOG.debug(\"  JWS Header: {}\", jws.getHeaders().getFullHeaderAsJsonString());\n            }\n\n            var jb = new JSONBuilder();\n            jb.put(\"protected\", jws.getHeaders().getEncodedHeader());\n            jb.put(\"payload\", jws.getEncodedPayload());\n            jb.put(\"signature\", jws.getEncodedSignature());\n            return jb;\n        } catch (JoseException ex) {\n            throw new IllegalArgumentException(\"Could not create a JOSE request\", ex);\n        }\n    }\n\n    /**\n     * Creates a JSON structure for external account binding.\n     *\n     * @param kid\n     *         Key Identifier provided by the CA\n     * @param accountKey\n     *         {@link PublicKey} of the account to register\n     * @param macKey\n     *         {@link SecretKey} to sign the key identifier with\n     * @param macAlgorithm\n     *         Algorithm of the MAC key\n     * @param resource\n     *         \"newAccount\" resource URL\n     * @return Created JSON structure\n     */\n    public static Map<String, Object> createExternalAccountBinding(String kid,\n            PublicKey accountKey, SecretKey macKey, String macAlgorithm, URL resource) {\n        try {\n            var keyJwk = PublicJsonWebKey.Factory.newPublicJwk(accountKey);\n\n            var innerJws = new JsonWebSignature();\n            innerJws.setPayload(keyJwk.toJson());\n            innerJws.getHeaders().setObjectHeaderValue(\"url\", resource);\n            innerJws.getHeaders().setObjectHeaderValue(\"kid\", kid);\n            innerJws.setAlgorithmHeaderValue(macAlgorithm);\n            innerJws.setKey(macKey);\n            innerJws.setDoKeyValidation(false);\n            innerJws.sign();\n\n            var outerClaim = new JSONBuilder();\n            outerClaim.put(\"protected\", innerJws.getHeaders().getEncodedHeader());\n            outerClaim.put(\"signature\", innerJws.getEncodedSignature());\n            outerClaim.put(\"payload\", innerJws.getEncodedPayload());\n            return outerClaim.toMap();\n        } catch (JoseException ex) {\n            throw new IllegalArgumentException(\"Could not create external account binding\", ex);\n        }\n    }\n\n    /**\n     * Converts a {@link PublicKey} to a JOSE JWK structure.\n     *\n     * @param key\n     *         {@link PublicKey} to convert\n     * @return JSON map containing the JWK structure\n     */\n    public static Map<String, Object> publicKeyToJWK(PublicKey key) {\n        try {\n            return PublicJsonWebKey.Factory.newPublicJwk(key)\n                    .toParams(JsonWebKey.OutputControlLevel.PUBLIC_ONLY);\n        } catch (JoseException ex) {\n            throw new IllegalArgumentException(\"Bad public key\", ex);\n        }\n    }\n\n    /**\n     * Converts a JOSE JWK structure to a {@link PublicKey}.\n     *\n     * @param jwk\n     *         Map containing a JWK structure\n     * @return the extracted {@link PublicKey}\n     */\n    public static PublicKey jwkToPublicKey(Map<String, Object> jwk) {\n        try {\n            return PublicJsonWebKey.Factory.newPublicJwk(jwk).getPublicKey();\n        } catch (JoseException ex) {\n            throw new IllegalArgumentException(\"Bad JWK\", ex);\n        }\n    }\n\n    /**\n     * Computes a thumbprint of the given public key.\n     *\n     * @param key\n     *         {@link PublicKey} to get the thumbprint of\n     * @return Thumbprint of the key\n     */\n    public static byte[] thumbprint(PublicKey key) {\n        try {\n            var jwk = PublicJsonWebKey.Factory.newPublicJwk(key);\n            return jwk.calculateThumbprint(\"SHA-256\");\n        } catch (JoseException ex) {\n            throw new IllegalArgumentException(\"Bad public key\", ex);\n        }\n    }\n\n    /**\n     * Analyzes the key used in the {@link JsonWebKey}, and returns the key algorithm\n     * identifier for {@link JsonWebSignature}.\n     *\n     * @param jwk\n     *         {@link JsonWebKey} to analyze\n     * @return algorithm identifier\n     * @throws IllegalArgumentException\n     *         there is no corresponding algorithm identifier for the key\n     */\n    public static String keyAlgorithm(JsonWebKey jwk) {\n        if (jwk instanceof EllipticCurveJsonWebKey ecjwk) {\n            return switch (ecjwk.getCurveName()) {\n                case \"P-256\" -> AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256;\n                case \"P-384\" -> AlgorithmIdentifiers.ECDSA_USING_P384_CURVE_AND_SHA384;\n                case \"P-521\" -> AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512;\n                default -> throw new IllegalArgumentException(\"Unknown EC name \" + ecjwk.getCurveName());\n            };\n\n        } else if (jwk instanceof RsaJsonWebKey) {\n            return AlgorithmIdentifiers.RSA_USING_SHA256;\n\n        } else {\n            throw new IllegalArgumentException(\"Unknown algorithm \" + jwk.getAlgorithm());\n        }\n    }\n\n    /**\n     * Analyzes the {@link SecretKey}, and returns the key algorithm identifier for {@link\n     * JsonWebSignature}.\n     *\n     * @param macKey\n     *         {@link SecretKey} to analyze\n     * @return algorithm identifier\n     * @throws IllegalArgumentException\n     *         there is no corresponding algorithm identifier for the key\n     */\n    public static String macKeyAlgorithm(SecretKey macKey) {\n        if (!\"HMAC\".equals(macKey.getAlgorithm())) {\n            throw new IllegalArgumentException(\"Bad algorithm: \" + macKey.getAlgorithm());\n        }\n\n        var size = macKey.getEncoded().length * 8;\n        if(size < 256) {\n            throw new IllegalArgumentException(\"Bad key size: \" + size);\n        }\n        if (size >= 512) {\n            return AlgorithmIdentifiers.HMAC_SHA512;\n        } else if (size >= 384) {\n            return AlgorithmIdentifiers.HMAC_SHA384;\n        } else {\n            return AlgorithmIdentifiers.HMAC_SHA256;\n        }\n    }\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * Internal toolbox. The API of these classes may change anytime, in a breaking manner,\n * and without prior notice. It is better not to use them in your project.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.toolbox;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.util;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static java.util.Objects.requireNonNull;\nimport static java.util.stream.Collectors.joining;\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.toAce;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.Writer;\nimport java.net.InetAddress;\nimport java.security.KeyPair;\nimport java.security.interfaces.ECKey;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Objects;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;\nimport org.bouncycastle.asn1.x500.X500Name;\nimport org.bouncycastle.asn1.x500.X500NameBuilder;\nimport org.bouncycastle.asn1.x500.style.BCStyle;\nimport org.bouncycastle.asn1.x509.Extension;\nimport org.bouncycastle.asn1.x509.ExtensionsGenerator;\nimport org.bouncycastle.asn1.x509.GeneralName;\nimport org.bouncycastle.asn1.x509.GeneralNames;\nimport org.bouncycastle.operator.OperatorCreationException;\nimport org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;\nimport org.bouncycastle.pkcs.PKCS10CertificationRequest;\nimport org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;\nimport org.bouncycastle.util.io.pem.PemObject;\nimport org.bouncycastle.util.io.pem.PemWriter;\nimport org.shredzone.acme4j.Identifier;\n\n/**\n * Generator for a CSR (Certificate Signing Request) suitable for ACME servers.\n * <p>\n * Requires {@code Bouncy Castle}. The\n * {@link org.bouncycastle.jce.provider.BouncyCastleProvider} must be added as security\n * provider.\n */\npublic class CSRBuilder {\n    private static final String SIGNATURE_ALG = \"SHA256withRSA\";\n    private static final String EC_SIGNATURE_ALG = \"SHA256withECDSA\";\n\n    private final X500NameBuilder namebuilder = new X500NameBuilder(X500Name.getDefaultStyle());\n    private final List<String> namelist = new ArrayList<>();\n    private final List<InetAddress> iplist = new ArrayList<>();\n    private @Nullable PKCS10CertificationRequest csr = null;\n    private boolean hasCnSet = false;\n\n    /**\n     * Adds a domain name to the CSR. All domain names will be added as <em>Subject\n     * Alternative Name</em>.\n     * <p>\n     * IDN domain names are ACE encoded automatically.\n     * <p>\n     * For wildcard certificates, the domain name must be prefixed with {@code \"*.\"}.\n     *\n     * @param domain\n     *         Domain name to add\n     */\n    public void addDomain(String domain) {\n        namelist.add(toAce(requireNonNull(domain)));\n    }\n\n    /**\n     * Adds a {@link Collection} of domains.\n     * <p>\n     * IDN domain names are ACE encoded automatically.\n     *\n     * @param domains\n     *            Collection of domain names to add\n     */\n    public void addDomains(Collection<String> domains) {\n        domains.forEach(this::addDomain);\n    }\n\n    /**\n     * Adds multiple domain names.\n     * <p>\n     * IDN domain names are ACE encoded automatically.\n     *\n     * @param domains\n     *            Domain names to add\n     */\n    public void addDomains(String... domains) {\n        Arrays.stream(domains).forEach(this::addDomain);\n    }\n\n    /**\n     * Adds an {@link InetAddress}. All IP addresses will be set as iPAddress <em>Subject\n     * Alternative Name</em>.\n     *\n     * @param address\n     *            {@link InetAddress} to add\n     * @since 2.4\n     */\n    public void addIP(InetAddress address) {\n        iplist.add(requireNonNull(address));\n    }\n\n    /**\n     * Adds a {@link Collection} of IP addresses.\n     *\n     * @param ips\n     *            Collection of IP addresses to add\n     * @since 2.4\n     */\n    public void addIPs(Collection<InetAddress> ips) {\n        ips.forEach(this::addIP);\n    }\n\n    /**\n     * Adds multiple IP addresses.\n     *\n     * @param ips\n     *            IP addresses to add\n     * @since 2.4\n     */\n    public void addIPs(InetAddress... ips) {\n        Arrays.stream(ips).forEach(this::addIP);\n    }\n\n    /**\n     * Adds an {@link Identifier}. Only DNS and IP types are supported.\n     *\n     * @param id\n     *            {@link Identifier} to add\n     * @since 2.7\n     */\n    public void addIdentifier(Identifier id) {\n        requireNonNull(id);\n        if (Identifier.TYPE_DNS.equals(id.getType())) {\n            addDomain(id.getDomain());\n        } else if (Identifier.TYPE_IP.equals(id.getType())) {\n            addIP(id.getIP());\n        } else {\n            throw new IllegalArgumentException(\"Unknown identifier type: \" + id.getType());\n        }\n    }\n\n    /**\n     * Adds a {@link Collection} of {@link Identifier}.\n     *\n     * @param ids\n     *            Collection of Identifiers to add\n     * @since 2.7\n     */\n    public void addIdentifiers(Collection<Identifier> ids) {\n        ids.forEach(this::addIdentifier);\n    }\n\n    /**\n     * Adds multiple {@link Identifier}.\n     *\n     * @param ids\n     *            Identifiers to add\n     * @since 2.7\n     */\n    public void addIdentifiers(Identifier... ids) {\n        Arrays.stream(ids).forEach(this::addIdentifier);\n    }\n\n    /**\n     * Sets an entry of the subject used for the CSR.\n     * <p>\n     * This method is meant as \"expert mode\" for setting attributes that are not covered\n     * by the other methods. It is at the discretion of the ACME server to accept this\n     * parameter.\n     *\n     * @param attName\n     *         The BCStyle attribute name\n     * @param value\n     *         The value\n     * @since 2.14\n     */\n    public void addValue(String attName, String value) {\n        var oid = X500Name.getDefaultStyle().attrNameToOID(requireNonNull(attName, \"attribute name must not be null\"));\n        addValue(oid, value);\n    }\n\n    /**\n     * Sets an entry of the subject used for the CSR.\n     * <p>\n     * This method is meant as \"expert mode\" for setting attributes that are not covered\n     * by the other methods. It is at the discretion of the ACME server to accept this\n     * parameter.\n     *\n     * @param oid\n     *         The OID of the attribute to be added\n     * @param value\n     *         The value\n     * @since 2.14\n     */\n    public void addValue(ASN1ObjectIdentifier oid, String value) {\n        if (requireNonNull(oid, \"OID must not be null\").equals(BCStyle.CN)) {\n            addDomain(value);\n            if (hasCnSet) {\n                return;\n            }\n            hasCnSet = true;\n        }\n        namebuilder.addRDN(oid, requireNonNull(value, \"attribute value must not be null\"));\n    }\n\n    /**\n     * Sets the common name.\n     * <p>\n     * Note that it is at the discretion of the ACME server to accept this parameter.\n     *\n     * @since 3.2.0\n     */\n    public void setCommonName(String cn) {\n        namebuilder.addRDN(BCStyle.CN, requireNonNull(cn));\n    }\n\n    /**\n     * Sets the organization.\n     * <p>\n     * Note that it is at the discretion of the ACME server to accept this parameter.\n     */\n    public void setOrganization(String o) {\n        namebuilder.addRDN(BCStyle.O, requireNonNull(o));\n    }\n\n    /**\n     * Sets the organizational unit.\n     * <p>\n     * Note that it is at the discretion of the ACME server to accept this parameter.\n     */\n    public void setOrganizationalUnit(String ou) {\n        namebuilder.addRDN(BCStyle.OU, requireNonNull(ou));\n    }\n\n    /**\n     * Sets the city or locality.\n     * <p>\n     * Note that it is at the discretion of the ACME server to accept this parameter.\n     */\n    public void setLocality(String l) {\n        namebuilder.addRDN(BCStyle.L, requireNonNull(l));\n    }\n\n    /**\n     * Sets the state or province.\n     * <p>\n     * Note that it is at the discretion of the ACME server to accept this parameter.\n     */\n    public void setState(String st) {\n        namebuilder.addRDN(BCStyle.ST, requireNonNull(st));\n    }\n\n    /**\n     * Sets the country.\n     * <p>\n     * Note that it is at the discretion of the ACME server to accept this parameter.\n     */\n    public void setCountry(String c) {\n        namebuilder.addRDN(BCStyle.C, requireNonNull(c));\n    }\n\n    /**\n     * Signs the completed CSR.\n     *\n     * @param keypair\n     *            {@link KeyPair} to sign the CSR with\n     */\n    public void sign(KeyPair keypair) throws IOException {\n        Objects.requireNonNull(keypair, \"keypair\");\n        if (namelist.isEmpty() && iplist.isEmpty()) {\n            throw new IllegalStateException(\"No domain or IP address was set\");\n        }\n\n        try {\n            var ix = 0;\n            var gns = new GeneralName[namelist.size() + iplist.size()];\n            for (var name : namelist) {\n                gns[ix++] = new GeneralName(GeneralName.dNSName, name);\n            }\n            for (var ip : iplist) {\n                gns[ix++] = new GeneralName(GeneralName.iPAddress, ip.getHostAddress());\n            }\n            var subjectAltName = new GeneralNames(gns);\n\n            var p10Builder = new JcaPKCS10CertificationRequestBuilder(namebuilder.build(), keypair.getPublic());\n\n            var extensionsGenerator = new ExtensionsGenerator();\n            extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, subjectAltName);\n\n            p10Builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate());\n\n            var pk = keypair.getPrivate();\n            var csBuilder = new JcaContentSignerBuilder(pk instanceof ECKey ? EC_SIGNATURE_ALG : SIGNATURE_ALG);\n            var signer = csBuilder.build(pk);\n\n            csr = p10Builder.build(signer);\n        } catch (OperatorCreationException ex) {\n            throw new IOException(\"Could not generate CSR\", ex);\n        }\n    }\n\n    /**\n     * Gets the PKCS#10 certification request.\n     */\n    public PKCS10CertificationRequest getCSR() {\n        if (csr == null) {\n            throw new IllegalStateException(\"sign CSR first\");\n        }\n\n        return csr;\n    }\n\n    /**\n     * Gets an encoded PKCS#10 certification request.\n     */\n    public byte[] getEncoded() throws IOException {\n        return getCSR().getEncoded();\n    }\n\n    /**\n     * Writes the signed certificate request to a {@link Writer}.\n     *\n     * @param w\n     *            {@link Writer} to write the PEM file to. The {@link Writer} is closed\n     *            after use.\n     */\n    public void write(Writer w) throws IOException {\n        if (csr == null) {\n            throw new IllegalStateException(\"sign CSR first\");\n        }\n\n        try (var pw = new PemWriter(w)) {\n            pw.writeObject(new PemObject(\"CERTIFICATE REQUEST\", getEncoded()));\n        }\n    }\n\n    /**\n     * Writes the signed certificate request to an {@link OutputStream}.\n     *\n     * @param out\n     *            {@link OutputStream} to write the PEM file to. The {@link OutputStream}\n     *            is closed after use.\n     */\n    public void write(OutputStream out) throws IOException {\n        write(new OutputStreamWriter(out, UTF_8));\n    }\n\n    @Override\n    public String toString() {\n        var sb = new StringBuilder();\n        sb.append(namebuilder.build());\n        if (!namelist.isEmpty()) {\n            sb.append(namelist.stream().collect(joining(\",DNS=\", \",DNS=\", \"\")));\n        }\n        if (!iplist.isEmpty()) {\n            sb.append(iplist.stream()\n                    .map(InetAddress::getHostAddress)\n                    .collect(joining(\",IP=\", \",IP=\", \"\")));\n        }\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.util;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.math.BigInteger;\nimport java.nio.charset.StandardCharsets;\nimport java.security.InvalidKeyException;\nimport java.security.KeyPair;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\nimport java.security.cert.CertificateException;\nimport java.security.cert.CertificateFactory;\nimport java.security.cert.X509Certificate;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Date;\nimport java.util.Objects;\nimport java.util.function.Function;\n\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.DEROctetString;\nimport org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;\nimport org.bouncycastle.asn1.x500.X500Name;\nimport org.bouncycastle.asn1.x509.Extension;\nimport org.bouncycastle.asn1.x509.Extensions;\nimport org.bouncycastle.asn1.x509.GeneralName;\nimport org.bouncycastle.asn1.x509.GeneralNames;\nimport org.bouncycastle.cert.CertIOException;\nimport org.bouncycastle.cert.X509CertificateHolder;\nimport org.bouncycastle.cert.jcajce.JcaX509v1CertificateBuilder;\nimport org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;\nimport org.bouncycastle.openssl.PEMParser;\nimport org.bouncycastle.operator.ContentSigner;\nimport org.bouncycastle.operator.OperatorCreationException;\nimport org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;\nimport org.bouncycastle.pkcs.PKCS10CertificationRequest;\nimport org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.challenge.TlsAlpn01Challenge;\n\n/**\n * Utility class offering convenience methods for certificates.\n * <p>\n * Requires {@code Bouncy Castle}.\n */\npublic final class CertificateUtils {\n\n    /**\n     * The {@code acmeValidation} object identifier.\n     *\n     * @since 2.1\n     */\n    public static final ASN1ObjectIdentifier ACME_VALIDATION =\n                    new ASN1ObjectIdentifier(TlsAlpn01Challenge.ACME_VALIDATION_OID).intern();\n\n    private CertificateUtils() {\n        // utility class without constructor\n    }\n\n    /**\n     * Reads a CSR PEM file.\n     *\n     * @param in\n     *            {@link InputStream} to read the CSR from. The {@link InputStream} is\n     *            closed after use.\n     * @return CSR that was read\n     */\n    public static PKCS10CertificationRequest readCSR(InputStream in) throws IOException {\n        try (var pemParser = new PEMParser(new InputStreamReader(in, StandardCharsets.US_ASCII))) {\n            var parsedObj = pemParser.readObject();\n            if (!(parsedObj instanceof PKCS10CertificationRequest)) {\n                throw new IOException(\"Not a PKCS10 CSR\");\n            }\n            return (PKCS10CertificationRequest) parsedObj;\n        }\n    }\n\n    /**\n     * Creates a self-signed {@link X509Certificate} that can be used for the\n     * {@link TlsAlpn01Challenge}. The certificate is valid for 7 days.\n     *\n     * @param keypair\n     *            A domain {@link KeyPair} to be used for the challenge\n     * @param id\n     *            The {@link Identifier} that is to be validated\n     * @param acmeValidation\n     *            The value that is returned by\n     *            {@link TlsAlpn01Challenge#getAcmeValidation()}\n     * @return Created certificate\n     * @since 2.6\n     */\n    public static X509Certificate createTlsAlpn01Certificate(KeyPair keypair, Identifier id, byte[] acmeValidation)\n                throws IOException {\n        Objects.requireNonNull(keypair, \"keypair\");\n        Objects.requireNonNull(id, \"id\");\n        if (acmeValidation == null || acmeValidation.length != 32) {\n            throw new IllegalArgumentException(\"Bad acmeValidation parameter\");\n        }\n\n        var now = System.currentTimeMillis();\n\n        var issuer = new X500Name(\"CN=acme.invalid\");\n        var serial = BigInteger.valueOf(now);\n        var notBefore = Instant.ofEpochMilli(now);\n        var notAfter = notBefore.plus(Duration.ofDays(7));\n\n        var certBuilder = new JcaX509v3CertificateBuilder(\n                    issuer, serial, Date.from(notBefore), Date.from(notAfter),\n                    issuer, keypair.getPublic());\n\n        var gns = new GeneralName[1];\n\n        gns[0] = switch (id.getType()) {\n            case Identifier.TYPE_DNS -> new GeneralName(GeneralName.dNSName, id.getDomain());\n            case Identifier.TYPE_IP -> new GeneralName(GeneralName.iPAddress, id.getIP().getHostAddress());\n            default -> throw new IllegalArgumentException(\"Unsupported Identifier type \" + id.getType());\n        };\n        certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(gns));\n        certBuilder.addExtension(ACME_VALIDATION, true, new DEROctetString(acmeValidation));\n\n        return buildCertificate(certBuilder::build, keypair.getPrivate());\n    }\n\n    /**\n     * Creates a self-signed root certificate.\n     * <p>\n     * The generated certificate is only meant for testing purposes!\n     *\n     * @param subject\n     *         This certificate's subject X.500 name.\n     * @param notBefore\n     *         {@link Instant} before which the certificate is not valid.\n     * @param notAfter\n     *         {@link Instant} after which the certificate is not valid.\n     * @param keypair\n     *         {@link KeyPair} that is to be used for this certificate.\n     * @return Generated {@link X509Certificate}\n     * @since 2.8\n     */\n    public static X509Certificate createTestRootCertificate(String subject,\n            Instant notBefore, Instant notAfter, KeyPair keypair) {\n        Objects.requireNonNull(subject, \"subject\");\n        Objects.requireNonNull(notBefore, \"notBefore\");\n        Objects.requireNonNull(notAfter, \"notAfter\");\n        Objects.requireNonNull(keypair, \"keypair\");\n\n        var certBuilder = new JcaX509v1CertificateBuilder(\n                new X500Name(subject),\n                BigInteger.valueOf(System.currentTimeMillis()),\n                Date.from(notBefore),\n                Date.from(notAfter),\n                new X500Name(subject),\n                keypair.getPublic()\n        );\n\n        return buildCertificate(certBuilder::build, keypair.getPrivate());\n    }\n\n    /**\n     * Creates an intermediate certificate that is signed by an issuer.\n     * <p>\n     * The generated certificate is only meant for testing purposes!\n     *\n     * @param subject\n     *         This certificate's subject X.500 name.\n     * @param notBefore\n     *         {@link Instant} before which the certificate is not valid.\n     * @param notAfter\n     *         {@link Instant} after which the certificate is not valid.\n     * @param intermediatePublicKey\n     *         {@link PublicKey} of this certificate\n     * @param issuer\n     *         The issuer's {@link X509Certificate}.\n     * @param issuerPrivateKey\n     *         {@link PrivateKey} of the issuer. This is not the private key of this\n     *         intermediate certificate.\n     * @return Generated {@link X509Certificate}\n     * @since 2.8\n     */\n    public static X509Certificate createTestIntermediateCertificate(String subject,\n            Instant notBefore, Instant notAfter, PublicKey intermediatePublicKey,\n            X509Certificate issuer, PrivateKey issuerPrivateKey) {\n        Objects.requireNonNull(subject, \"subject\");\n        Objects.requireNonNull(notBefore, \"notBefore\");\n        Objects.requireNonNull(notAfter, \"notAfter\");\n        Objects.requireNonNull(intermediatePublicKey, \"intermediatePublicKey\");\n        Objects.requireNonNull(issuer, \"issuer\");\n        Objects.requireNonNull(issuerPrivateKey, \"issuerPrivateKey\");\n\n        var certBuilder = new JcaX509v1CertificateBuilder(\n                new X500Name(issuer.getIssuerX500Principal().getName()),\n                BigInteger.valueOf(System.currentTimeMillis()),\n                Date.from(notBefore),\n                Date.from(notAfter),\n                new X500Name(subject),\n                intermediatePublicKey\n        );\n\n        return buildCertificate(certBuilder::build, issuerPrivateKey);\n    }\n\n    /**\n     * Creates a signed end entity certificate from the given CSR.\n     * <p>\n     * This method is only meant for testing purposes! Do not use it in a real-world CA\n     * implementation.\n     * <p>\n     * Do not assume that real-world certificates have a similar structure. It's up to the\n     * discretion of the CA which distinguished names, validity dates, extensions and\n     * other parameters are transferred from the CSR to the generated certificate.\n     *\n     * @param csr\n     *         CSR to create the certificate from\n     * @param notBefore\n     *         {@link Instant} before which the certificate is not valid.\n     * @param notAfter\n     *         {@link Instant} after which the certificate is not valid.\n     * @param issuer\n     *         The issuer's {@link X509Certificate}.\n     * @param issuerPrivateKey\n     *         {@link PrivateKey} of the issuer. This is not the private key the CSR was\n     *         signed with.\n     * @return Generated {@link X509Certificate}\n     * @since 2.8\n     */\n    public static X509Certificate createTestCertificate(PKCS10CertificationRequest csr,\n            Instant notBefore, Instant notAfter, X509Certificate issuer, PrivateKey issuerPrivateKey) {\n        Objects.requireNonNull(csr, \"csr\");\n        Objects.requireNonNull(notBefore, \"notBefore\");\n        Objects.requireNonNull(notAfter, \"notAfter\");\n        Objects.requireNonNull(issuer, \"issuer\");\n        Objects.requireNonNull(issuerPrivateKey, \"issuerPrivateKey\");\n\n        try {\n            var jcaCsr = new JcaPKCS10CertificationRequest(csr);\n\n            var certBuilder = new JcaX509v3CertificateBuilder(\n                    new X500Name(issuer.getIssuerX500Principal().getName()),\n                    BigInteger.valueOf(System.currentTimeMillis()),\n                    Date.from(notBefore),\n                    Date.from(notAfter),\n                    csr.getSubject(),\n                    jcaCsr.getPublicKey());\n\n            var attr = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);\n            if (attr.length > 0) {\n                var extensions = attr[0].getAttrValues().toArray();\n                if (extensions.length > 0 && extensions[0] instanceof Extensions extension0) {\n                    var san = GeneralNames.fromExtensions(extension0, Extension.subjectAlternativeName);\n                    var critical = csr.getSubject().getRDNs().length == 0;\n                    certBuilder.addExtension(Extension.subjectAlternativeName, critical, san);\n                }\n            }\n\n            return buildCertificate(certBuilder::build, issuerPrivateKey);\n        } catch (NoSuchAlgorithmException | InvalidKeyException | CertIOException ex) {\n            throw new IllegalArgumentException(\"Invalid CSR\", ex);\n        }\n    }\n\n    /**\n     * Build a {@link X509Certificate} from a builder.\n     *\n     * @param builder\n     *         Builder method that receives a {@link ContentSigner} and returns a {@link\n     *         X509CertificateHolder}.\n     * @param privateKey\n     *         {@link PrivateKey} to sign the certificate with\n     * @return The generated {@link X509Certificate}\n     */\n    private static X509Certificate buildCertificate(Function<ContentSigner, X509CertificateHolder> builder, PrivateKey privateKey) {\n        try {\n            var signerBuilder = new JcaContentSignerBuilder(\"SHA256withRSA\");\n            var cert = builder.apply(signerBuilder.build(privateKey)).getEncoded();\n            var certificateFactory = CertificateFactory.getInstance(\"X.509\");\n            return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(cert));\n        } catch (CertificateException | OperatorCreationException | IOException ex) {\n            throw new IllegalArgumentException(\"Could not build certificate\", ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/util/KeyPairUtils.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.util;\n\nimport java.io.IOException;\nimport java.io.Reader;\nimport java.io.Writer;\nimport java.security.InvalidAlgorithmParameterException;\nimport java.security.KeyPair;\nimport java.security.KeyPairGenerator;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.NoSuchProviderException;\nimport java.security.PublicKey;\nimport java.security.SecureRandom;\n\nimport org.bouncycastle.jce.ECNamedCurveTable;\nimport org.bouncycastle.jce.provider.BouncyCastleProvider;\nimport org.bouncycastle.openssl.PEMException;\nimport org.bouncycastle.openssl.PEMKeyPair;\nimport org.bouncycastle.openssl.PEMParser;\nimport org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;\nimport org.bouncycastle.openssl.jcajce.JcaPEMWriter;\n\n/**\n * Utility class offering convenience methods for {@link KeyPair}.\n * <p>\n * Requires {@code Bouncy Castle}.\n */\npublic class KeyPairUtils {\n\n    private KeyPairUtils() {\n        // utility class without constructor\n    }\n\n    /**\n     * Creates a new standard {@link KeyPair}.\n     * <p>\n     * This method can be used if no specific key type is required. It returns a\n     * \"secp384r1\" ECDSA key pair.\n     *\n     * @return Generated {@link KeyPair}\n     * @since 2.8\n     */\n    public static KeyPair createKeyPair() {\n        return createECKeyPair(\"secp384r1\");\n    }\n\n    /**\n     * Creates a new RSA {@link KeyPair}.\n     *\n     * @param keysize\n     *            Key size\n     * @return Generated {@link KeyPair}\n     */\n    public static KeyPair createKeyPair(int keysize) {\n        try {\n            var keyGen = KeyPairGenerator.getInstance(\"RSA\");\n            keyGen.initialize(keysize);\n            return keyGen.generateKeyPair();\n        } catch (NoSuchAlgorithmException ex) {\n            throw new IllegalStateException(ex);\n        }\n    }\n\n    /**\n     * Creates a new elliptic curve {@link KeyPair}.\n     *\n     * @param name\n     *            ECDSA curve name (e.g. \"secp256r1\")\n     * @return Generated {@link KeyPair}\n     */\n    public static KeyPair createECKeyPair(String name) {\n        try {\n            var ecSpec = ECNamedCurveTable.getParameterSpec(name);\n            var g = KeyPairGenerator.getInstance(\"ECDSA\", BouncyCastleProvider.PROVIDER_NAME);\n            g.initialize(ecSpec, new SecureRandom());\n            return g.generateKeyPair();\n        } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException ex) {\n            throw new IllegalArgumentException(\"Invalid curve name \" + name, ex);\n        } catch (NoSuchProviderException ex) {\n            throw new IllegalStateException(ex);\n        }\n    }\n\n    /**\n     * Reads a {@link KeyPair} from a PEM file.\n     *\n     * @param r\n     *            {@link Reader} to read the PEM file from. The {@link Reader} is closed\n     *            after use.\n     * @return {@link KeyPair} read\n     */\n    public static KeyPair readKeyPair(Reader r) throws IOException {\n        try (var parser = new PEMParser(r)) {\n            var keyPair = (PEMKeyPair) parser.readObject();\n            return new JcaPEMKeyConverter().getKeyPair(keyPair);\n        } catch (PEMException ex) {\n            throw new IOException(\"Invalid PEM file\", ex);\n        }\n    }\n\n    /**\n     * Writes a {@link KeyPair} PEM file.\n     *\n     * @param keypair\n     *            {@link KeyPair} to write\n     * @param w\n     *            {@link Writer} to write the PEM file to. The {@link Writer} is closed\n     *            after use.\n     */\n    public static void writeKeyPair(KeyPair keypair, Writer w) throws IOException {\n        try (var jw = new JcaPEMWriter(w)) {\n            jw.writeObject(keypair);\n        }\n    }\n\n    /**\n     * Writes a {@link PublicKey} as PEM file.\n     *\n     * @param key\n     *            {@link PublicKey}\n     * @param w\n     *            {@link Writer} to write the PEM file to. The {@link Writer} is closed\n     *            after use.\n     * @since 3.0.0\n     */\n    public static void writePublicKey(PublicKey key, Writer w) throws IOException {\n        try (var jw = new JcaPEMWriter(w)) {\n            jw.writeObject(key);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/main/java/org/shredzone/acme4j/util/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * A collection of utility classes. All of them require Bouncy Castle to be added as\n *  * security provider.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.util;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-client/src/main/resources/.gitignore",
    "content": ""
  },
  {
    "path": "acme4j-client/src/main/resources/META-INF/services/org.shredzone.acme4j.provider.AcmeProvider",
    "content": "\n# Actalis: https://www.actalis.com/\norg.shredzone.acme4j.provider.actalis.ActalisAcmeProvider\n\n# Google Trust Services: https://pki.goog/\norg.shredzone.acme4j.provider.google.GoogleAcmeProvider\n\n# Let's Encrypt: https://letsencrypt.org\norg.shredzone.acme4j.provider.letsencrypt.LetsEncryptAcmeProvider\n\n# Pebble (ACME Test Server): https://github.com/letsencrypt/pebble\norg.shredzone.acme4j.provider.pebble.PebbleAcmeProvider\n\n# SSL.com: https://ssl.com\norg.shredzone.acme4j.provider.sslcom.SslComAcmeProvider\n\n# ZeroSSL: https://zerossl.com\norg.shredzone.acme4j.provider.zerossl.ZeroSSLAcmeProvider\n\n"
  },
  {
    "path": "acme4j-client/src/main/resources/org/shredzone/acme4j/provider/pebble/pebble.minica.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDPzCCAiegAwIBAgIIU0Xm9UFdQxUwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVbWluaWNhIHJvb3QgY2EgNTM0NWU2MCAXDTI1MDkwMzIzNDAwNVoYDzIxMjUw\nOTAzMjM0MDA1WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA1MzQ1ZTYwggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ\nalozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn\nAjm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu\n9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0\ntoumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3\nHy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB\nAAGjezB5MA4GA1UdDwEB/wQEAwIChDATBgNVHSUEDDAKBggrBgEFBQcDATASBgNV\nHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSu8RGpErgYUoYnQuwCq+/ggTiEjDAf\nBgNVHSMEGDAWgBSu8RGpErgYUoYnQuwCq+/ggTiEjDANBgkqhkiG9w0BAQsFAAOC\nAQEAXDVYov1+f6EL7S41LhYQkEX/GyNNzsEvqxE9U0+3Iri5JfkcNOiA9O9L6Z+Y\nbqcsXV93s3vi4r4WSWuc//wHyJYrVe5+tK4nlFpbJOvfBUtnoBDyKNxXzZCxFJVh\nf9uc8UejRfQMFbDbhWY/x83y9BDufJHHq32OjCIN7gp2UR8rnfYvlz7Zg4qkJBsn\nDG4dwd+pRTCFWJOVIG0JoNhK3ZmE7oJ1N4H38XkZ31NPcMksKxpsLLIS9+mosZtg\n4olL7tMPJklx5ZaeMFaKRDq4Gdxkbw4+O4vRgNm3Z8AXWKknOdfgdpqLUPPhRcP4\nv1lhy71EhBuXXwRQJry0lTdF+w==\n-----END CERTIFICATE-----"
  },
  {
    "path": "acme4j-client/src/main/resources-filtered/org/shredzone/acme4j/version.properties",
    "content": "version=${project.version}"
  },
  {
    "path": "acme4j-client/src/test/java/.gitignore",
    "content": ""
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/AccountBuilderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatException;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.util.Optional;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.jose4j.jwx.CompactSerializer;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.NullAndEmptySource;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.mockito.Mockito;\nimport org.shredzone.acme4j.connector.RequestSigner;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.provider.TestableConnectionProvider;\nimport org.shredzone.acme4j.toolbox.AcmeUtils;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.JoseUtilsTest;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link AccountBuilder}.\n */\npublic class AccountBuilderTest {\n\n    private final URL resourceUrl = url(\"http://example.com/acme/resource\");\n    private final URL locationUrl = url(\"http://example.com/acme/account\");\n\n    /**\n     * Test if a new account can be created.\n     */\n    @Test\n    public void testRegistration() throws Exception {\n        var accountKey = TestUtils.createKeyPair();\n\n        var provider = new TestableConnectionProvider() {\n            private boolean isUpdate;\n\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(login).isNotNull();\n                assertThat(url).isEqualTo(locationUrl);\n                assertThat(isUpdate).isFalse();\n                isUpdate = true;\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Session session, RequestSigner signer) {\n                assertThat(session).isNotNull();\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"newAccount\").toString());\n                isUpdate = false;\n                return HttpURLConnection.HTTP_CREATED;\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"newAccountResponse\");\n            }\n        };\n\n        provider.putTestResource(Resource.NEW_ACCOUNT, resourceUrl);\n\n        var builder = new AccountBuilder();\n        builder.addContact(\"mailto:foo@example.com\");\n        builder.agreeToTermsOfService();\n        builder.useKeyPair(accountKey);\n\n        var session = provider.createSession();\n        var login = builder.createLogin(session);\n\n        var account = login.getAccount();\n        assertThat(account.getTermsOfServiceAgreed().orElseThrow()).isTrue();\n        assertThat(account.getLocation()).isEqualTo(locationUrl);\n        assertThat(account.hasExternalAccountBinding()).isFalse();\n        assertThat(account.getKeyIdentifier()).isEmpty();\n\n        provider.close();\n    }\n\n    /**\n     * Test if a new account with Key Identifier can be created.\n     */\n    @ParameterizedTest\n    @CsvSource({\n            // Derived from key size\n            \"SHA-256,HS256,,\",\n            \"SHA-384,HS384,,\",\n            \"SHA-512,HS512,,\",\n\n            // Enforced, but same as key size\n            \"SHA-256,HS256,HS256,\",\n            \"SHA-384,HS384,HS384,\",\n            \"SHA-512,HS512,HS512,\",\n\n            // Enforced, different from key size\n            \"SHA-512,HS256,HS256,\",\n\n            // Proposed by provider\n            \"SHA-256,HS256,,HS256\",\n            \"SHA-512,HS256,,HS256\",\n            \"SHA-512,HS512,HS512,HS256\",\n    })\n    public void testRegistrationWithKid(String keyAlg,\n                                        String expectedMacAlg,\n                                        @Nullable String macAlg,\n                                        @Nullable String providerAlg\n    ) throws Exception {\n        var accountKey = TestUtils.createKeyPair();\n        var keyIdentifier = \"NCC-1701\";\n        var macKey = TestUtils.createSecretKey(keyAlg);\n\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Session session, RequestSigner signer) {\n                assertThat(session).isNotNull();\n                assertThat(url).isEqualTo(resourceUrl);\n\n                var binding = claims.toJSON()\n                                .get(\"externalAccountBinding\")\n                                .asObject();\n\n                var encodedHeader = binding.get(\"protected\").asString();\n                var encodedSignature = binding.get(\"signature\").asString();\n                var encodedPayload = binding.get(\"payload\").asString();\n                var serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);\n\n                JoseUtilsTest.assertExternalAccountBinding(serialized, resourceUrl, keyIdentifier, macKey, expectedMacAlg);\n\n                return HttpURLConnection.HTTP_CREATED;\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return JSON.empty();\n            }\n\n            @Override\n            public Optional<String> getProposedEabMacAlgorithm() {\n                return Optional.ofNullable(providerAlg);\n            }\n        };\n\n        provider.putTestResource(Resource.NEW_ACCOUNT, resourceUrl);\n        provider.putMetadata(\"externalAccountRequired\", true);\n\n        var builder = new AccountBuilder();\n        builder.useKeyPair(accountKey);\n        builder.withKeyIdentifier(keyIdentifier, AcmeUtils.base64UrlEncode(macKey.getEncoded()));\n        if (macAlg != null) {\n            builder.withMacAlgorithm(macAlg);\n        }\n\n        var session = provider.createSession();\n        var login = builder.createLogin(session);\n\n        assertThat(login.getAccount().getLocation()).isEqualTo(locationUrl);\n\n        provider.close();\n    }\n\n    /**\n     * Test if invalid mac algorithms are rejected.\n     */\n    @ParameterizedTest\n    @NullAndEmptySource\n    @ValueSource(strings = {\"foo\", \"null\", \"false\", \"none\", \"HS-256\", \"hs256\", \"HS128\", \"RS256\"})\n    public void testRejectInvalidMacAlg(@Nullable String macAlg) {\n        assertThatException().isThrownBy(() -> {\n            new AccountBuilder().withMacAlgorithm(macAlg);\n        }).isInstanceOfAny(IllegalArgumentException.class, NullPointerException.class);\n    }\n\n    /**\n     * Test if an existing account is properly returned.\n     */\n    @Test\n    public void testOnlyExistingRegistration() throws Exception {\n        var accountKey = TestUtils.createKeyPair();\n\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Session session, RequestSigner signer) {\n                assertThat(session).isNotNull();\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"newAccountOnlyExisting\").toString());\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"newAccountResponse\");\n            }\n        };\n\n        provider.putTestResource(Resource.NEW_ACCOUNT, resourceUrl);\n\n        var builder = new AccountBuilder();\n        builder.useKeyPair(accountKey);\n        builder.onlyExisting();\n\n        var session = provider.createSession();\n        var login = builder.createLogin(session);\n\n        assertThat(login.getAccount().getLocation()).isEqualTo(locationUrl);\n\n        provider.close();\n    }\n\n    @Test\n    public void testEmailAddresses() {\n        var builder = Mockito.spy(AccountBuilder.class);\n        builder.addEmail(\"foo@example.com\");\n        Mockito.verify(builder).addContact(Mockito.eq(\"mailto:foo@example.com\"));\n\n        // mailto is still accepted if present\n        builder.addEmail(\"mailto:bar@example.com\");\n        Mockito.verify(builder).addContact(Mockito.eq(\"mailto:bar@example.com\"));\n    }\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/AccountTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static java.util.Collections.emptyList;\nimport static java.util.Collections.singletonList;\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.*;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.fail;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.io.IOException;\nimport java.net.HttpURLConnection;\nimport java.net.URI;\nimport java.net.URL;\nimport java.util.Collection;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport org.jose4j.jws.JsonWebSignature;\nimport org.jose4j.jwx.CompactSerializer;\nimport org.jose4j.lang.JoseException;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.challenge.Dns01Challenge;\nimport org.shredzone.acme4j.challenge.Http01Challenge;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeNotSupportedException;\nimport org.shredzone.acme4j.exception.AcmeServerException;\nimport org.shredzone.acme4j.provider.TestableConnectionProvider;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link Account}.\n */\npublic class AccountTest {\n\n    private final URL resourceUrl  = url(\"http://example.com/acme/resource\");\n    private final URL locationUrl  = url(TestUtils.ACCOUNT_URL);\n    private final URL agreementUrl = url(\"http://example.com/agreement.pdf\");\n\n    /**\n     * Test that a account can be updated.\n     */\n    @Test\n    public void testUpdateAccount() throws AcmeException, IOException {\n        var provider = new TestableConnectionProvider() {\n            private JSON jsonResponse;\n\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"updateAccount\").toString());\n                assertThat(login).isNotNull();\n                jsonResponse = getJSON(\"updateAccountResponse\");\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                if (\"https://example.com/acme/acct/1/orders\".equals(url.toExternalForm())) {\n                    jsonResponse = new JSONBuilder()\n                                .array(\"orders\", singletonList(\"https://example.com/acme/order/1\"))\n                                .toJSON();\n                } else {\n                    jsonResponse = getJSON(\"updateAccountResponse\");\n                }\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return jsonResponse;\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n\n            @Override\n            public Collection<URL> getLinks(String relation) {\n                return emptyList();\n            }\n        };\n\n        var login = provider.createLogin();\n        var account = new Account(login, locationUrl);\n        account.fetch();\n\n        assertThat(login.getAccount().getLocation()).isEqualTo(locationUrl);\n        assertThat(account.getLocation()).isEqualTo(locationUrl);\n        assertThat(account.getTermsOfServiceAgreed().orElseThrow()).isTrue();\n        assertThat(account.getContacts()).hasSize(1);\n        assertThat(account.getContacts().get(0)).isEqualTo(URI.create(\"mailto:foo2@example.com\"));\n        assertThat(account.getStatus()).isEqualTo(Status.VALID);\n        assertThat(account.hasExternalAccountBinding()).isTrue();\n        assertThat(account.getKeyIdentifier().orElseThrow()).isEqualTo(\"NCC-1701\");\n\n        var orderIt = account.getOrders();\n        assertThat(orderIt).isNotNull();\n        assertThat(orderIt.next().getLocation()).isEqualTo(url(\"https://example.com/acme/order/1\"));\n        assertThat(orderIt.hasNext()).isFalse();\n\n        provider.close();\n    }\n\n    /**\n     * Test lazy loading.\n     */\n    @Test\n    public void testLazyLoading() throws IOException {\n        var requestWasSent = new AtomicBoolean(false);\n\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                requestWasSent.set(true);\n                assertThat(url).isEqualTo(locationUrl);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"updateAccountResponse\");\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n\n            @Override\n            public Collection<URL> getLinks(String relation) {\n                return switch (relation) {\n                    case \"termsOfService\" -> singletonList(agreementUrl);\n                    default -> emptyList();\n                };\n            }\n        };\n\n        var account = new Account(provider.createLogin(), locationUrl);\n\n        // Lazy loading\n        assertThat(requestWasSent.get()).isFalse();\n        assertThat(account.getTermsOfServiceAgreed().orElseThrow()).isTrue();\n        assertThat(requestWasSent.get()).isTrue();\n\n        // Subsequent queries do not trigger another load\n        requestWasSent.set(false);\n        assertThat(account.getTermsOfServiceAgreed().orElseThrow()).isTrue();\n        assertThat(account.getStatus()).isEqualTo(Status.VALID);\n        assertThat(requestWasSent.get()).isFalse();\n\n        provider.close();\n    }\n\n    /**\n     * Test that a domain can be pre-authorized.\n     */\n    @Test\n    public void testPreAuthorizeDomain() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"newAuthorizationRequest\").toString());\n                assertThat(login).isNotNull();\n                return HttpURLConnection.HTTP_CREATED;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"newAuthorizationResponse\");\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putTestResource(Resource.NEW_AUTHZ, resourceUrl);\n        provider.putTestChallenge(Http01Challenge.TYPE, Http01Challenge::new);\n        provider.putTestChallenge(Dns01Challenge.TYPE, Dns01Challenge::new);\n\n        var domainName = \"example.org\";\n\n        var account = new Account(login, locationUrl);\n        var auth = account.preAuthorize(Identifier.dns(domainName));\n\n        assertThat(auth.getIdentifier().getDomain()).isEqualTo(domainName);\n        assertThat(auth.getStatus()).isEqualTo(Status.PENDING);\n        assertThat(auth.getExpires()).isEmpty();\n        assertThat(auth.getLocation()).isEqualTo(locationUrl);\n\n        assertThat(auth.getChallenges()).containsExactlyInAnyOrder(\n                        provider.getChallenge(Http01Challenge.TYPE),\n                        provider.getChallenge(Dns01Challenge.TYPE));\n\n        provider.close();\n    }\n\n    /**\n     * Test that pre-authorization with subdomains fails if not supported.\n     */\n    @Test\n    public void testPreAuthorizeDomainSubdomainsFails() throws Exception {\n        var provider = new TestableConnectionProvider();\n\n        var login = provider.createLogin();\n\n        provider.putTestResource(Resource.NEW_AUTHZ, resourceUrl);\n\n        assertThat(login.getSession().getMetadata().isSubdomainAuthAllowed()).isFalse();\n\n        var account = new Account(login, locationUrl);\n\n        assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() ->\n                account.preAuthorize(Identifier.dns(\"example.org\").allowSubdomainAuth())\n        );\n\n        provider.close();\n    }\n\n    /**\n     * Test that a domain can be pre-authorized, with allowed subdomains.\n     */\n    @Test\n    public void testPreAuthorizeDomainSubdomains() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"newAuthorizationRequestSub\").toString());\n                assertThat(login).isNotNull();\n                return HttpURLConnection.HTTP_CREATED;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"newAuthorizationResponseSub\");\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putMetadata(\"subdomainAuthAllowed\", true);\n        provider.putTestResource(Resource.NEW_AUTHZ, resourceUrl);\n        provider.putTestChallenge(Dns01Challenge.TYPE, Dns01Challenge::new);\n\n        var domainName = \"example.org\";\n\n        var account = new Account(login, locationUrl);\n        var auth = account.preAuthorize(Identifier.dns(domainName).allowSubdomainAuth());\n\n        assertThat(login.getSession().getMetadata().isSubdomainAuthAllowed()).isTrue();\n        assertThat(auth.getIdentifier().getDomain()).isEqualTo(domainName);\n        assertThat(auth.getStatus()).isEqualTo(Status.PENDING);\n        assertThat(auth.getExpires()).isEmpty();\n        assertThat(auth.getLocation()).isEqualTo(locationUrl);\n        assertThat(auth.isSubdomainAuthAllowed()).isTrue();\n\n        assertThat(auth.getChallenges()).containsExactlyInAnyOrder(\n                provider.getChallenge(Dns01Challenge.TYPE));\n\n        provider.close();\n    }\n\n    /**\n     * Test that a domain pre-authorization can fail.\n     */\n    @Test\n    public void testNoPreAuthorizeDomain() throws Exception {\n        var problemType = URI.create(\"urn:ietf:params:acme:error:rejectedIdentifier\");\n        var problemDetail = \"example.org is blacklisted\";\n\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) throws AcmeException {\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"newAuthorizationRequest\").toString());\n                assertThat(login).isNotNull();\n\n                var problem = TestUtils.createProblem(problemType, problemDetail, resourceUrl);\n                throw new AcmeServerException(problem);\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putTestResource(Resource.NEW_AUTHZ, resourceUrl);\n\n        var account = new Account(login, locationUrl);\n\n        var ex = assertThrows(AcmeServerException.class, () ->\n            account.preAuthorizeDomain(\"example.org\")\n        );\n        assertThat(ex.getType()).isEqualTo(problemType);\n        assertThat(ex.getMessage()).isEqualTo(problemDetail);\n\n        provider.close();\n    }\n\n    /**\n     * Test that a bad domain parameter is not accepted.\n     */\n    @Test\n    public void testAuthorizeBadDomain() throws Exception {\n        var provider = new TestableConnectionProvider();\n        // just provide a resource record so the provider returns a directory\n        provider.putTestResource(Resource.NEW_NONCE, resourceUrl);\n\n        var login = provider.createLogin();\n        var account = login.getAccount();\n\n        assertThatNullPointerException()\n                .isThrownBy(() -> account.preAuthorizeDomain(null));\n        assertThatIllegalArgumentException()\n                .isThrownBy(() -> account.preAuthorizeDomain(\"\"));\n        assertThatExceptionOfType(AcmeNotSupportedException.class)\n                .isThrownBy(() -> account.preAuthorizeDomain(\"example.com\"))\n                .withMessage(\"Server does not support newAuthz\");\n\n        provider.close();\n    }\n\n    /**\n     * Test that the account key can be changed.\n     */\n    @Test\n    public void testChangeKey() throws Exception {\n        var oldKeyPair = TestUtils.createKeyPair();\n        var newKeyPair = TestUtils.createDomainKeyPair();\n\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder payload, Login login) {\n                try {\n                    assertThat(url).isEqualTo(locationUrl);\n                    assertThat(login).isNotNull();\n                    assertThat(login.getPublicKey()).isSameAs(oldKeyPair.getPublic());\n\n                    var json = payload.toJSON();\n                    var encodedHeader = json.get(\"protected\").asString();\n                    var encodedSignature = json.get(\"signature\").asString();\n                    var encodedPayload = json.get(\"payload\").asString();\n\n                    var serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);\n                    var jws = new JsonWebSignature();\n                    jws.setCompactSerialization(serialized);\n                    jws.setKey(newKeyPair.getPublic());\n                    assertThat(jws.verifySignature()).isTrue();\n\n                    var decodedPayload = jws.getPayload();\n\n                    var expectedPayload = new StringBuilder();\n                    expectedPayload.append('{');\n                    expectedPayload.append(\"\\\"account\\\":\\\"\").append(locationUrl).append(\"\\\",\");\n                    expectedPayload.append(\"\\\"oldKey\\\":{\");\n                    expectedPayload.append(\"\\\"kty\\\":\\\"\").append(TestUtils.KTY).append(\"\\\",\");\n                    expectedPayload.append(\"\\\"e\\\":\\\"\").append(TestUtils.E).append(\"\\\",\");\n                    expectedPayload.append(\"\\\"n\\\":\\\"\").append(TestUtils.N).append(\"\\\"\");\n                    expectedPayload.append(\"}}\");\n                    assertThatJson(decodedPayload).isEqualTo(expectedPayload.toString());\n                } catch (JoseException ex) {\n                    fail(ex);\n                }\n\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n        };\n\n        provider.putTestResource(Resource.KEY_CHANGE, locationUrl);\n\n        var session = TestUtils.session(provider);\n        var login = new Login(locationUrl, oldKeyPair, session);\n\n        assertThat(login.getPublicKey()).isSameAs(oldKeyPair.getPublic());\n\n        var account = new Account(login, locationUrl);\n        account.changeKey(newKeyPair);\n\n        assertThat(login.getPublicKey()).isSameAs(newKeyPair.getPublic());\n    }\n\n    /**\n     * Test that the same account key is not accepted for change.\n     */\n    @Test\n    public void testChangeSameKey() {\n        assertThrows(IllegalArgumentException.class, () -> {\n            var provider = new TestableConnectionProvider();\n            var login = provider.createLogin();\n\n            var account = new Account(login, locationUrl);\n            account.changeKey(provider.getAccountKeyPair());\n\n            provider.close();\n        });\n    }\n\n    /**\n     * Test that an account can be deactivated.\n     */\n    @Test\n    public void testDeactivate() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                var json = claims.toJSON();\n                assertThat(json.get(\"status\").asString()).isEqualTo(\"deactivated\");\n                assertThat(url).isEqualTo(locationUrl);\n                assertThat(login).isNotNull();\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"deactivateAccountResponse\");\n            }\n        };\n\n        var account = new Account(provider.createLogin(), locationUrl);\n        account.deactivate();\n\n        assertThat(account.getStatus()).isEqualTo(Status.DEACTIVATED);\n\n        provider.close();\n    }\n\n    /**\n     * Test that a new order can be created.\n     */\n    @Test\n    public void testNewOrder() throws AcmeException, IOException {\n        var provider = new TestableConnectionProvider();\n        var login = provider.createLogin();\n\n        var account = new Account(login, locationUrl);\n        assertThat(account.newOrder()).isNotNull();\n\n        provider.close();\n    }\n\n    /**\n     * Test that an account can be modified.\n     */\n    @Test\n    public void testModify() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"modifyAccount\").toString());\n                assertThat(login).isNotNull();\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"modifyAccountResponse\");\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n        };\n\n        var account = new Account(provider.createLogin(), locationUrl);\n        account.setJSON(getJSON(\"newAccount\"));\n\n        var editable = account.modify();\n        assertThat(editable).isNotNull();\n\n        editable.addContact(\"mailto:foo2@example.com\");\n        editable.getContacts().add(URI.create(\"mailto:foo3@example.com\"));\n        editable.commit();\n\n        assertThat(account.getLocation()).isEqualTo(locationUrl);\n        assertThat(account.getContacts()).hasSize(3);\n        assertThat(account.getContacts()).element(0).isEqualTo(URI.create(\"mailto:foo@example.com\"));\n        assertThat(account.getContacts()).element(1).isEqualTo(URI.create(\"mailto:foo2@example.com\"));\n        assertThat(account.getContacts()).element(2).isEqualTo(URI.create(\"mailto:foo3@example.com\"));\n\n        provider.close();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/AcmeJsonResourceTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.io.Serial;\nimport java.net.URL;\nimport java.time.Instant;\nimport java.util.Optional;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link AcmeJsonResource}.\n */\npublic class AcmeJsonResourceTest {\n\n    private static final JSON JSON_DATA = getJSON(\"newAccountResponse\");\n    private static final URL LOCATION_URL = url(\"https://example.com/acme/resource/123\");\n\n    /**\n     * Test {@link AcmeJsonResource#AcmeJsonResource(Login, URL)}.\n     */\n    @Test\n    public void testLoginConstructor() {\n        var login = TestUtils.login();\n\n        var resource = new DummyJsonResource(login, LOCATION_URL);\n        assertThat(resource.getLogin()).isEqualTo(login);\n        assertThat(resource.getSession()).isEqualTo(login.getSession());\n        assertThat(resource.getLocation()).isEqualTo(LOCATION_URL);\n        assertThat(resource.isValid()).isFalse();\n        assertThat(resource.getRetryAfter()).isEmpty();\n        assertUpdateInvoked(resource, 0);\n\n        assertThat(resource.getJSON()).isEqualTo(JSON_DATA);\n        assertThat(resource.isValid()).isTrue();\n        assertUpdateInvoked(resource, 1);\n    }\n\n    /**\n     * Test {@link AcmeJsonResource#setJSON(JSON)}.\n     */\n    @Test\n    public void testSetJson() {\n        var login = TestUtils.login();\n\n        var jsonData2 = getJSON(\"requestOrderResponse\");\n\n        var resource = new DummyJsonResource(login, LOCATION_URL);\n        assertThat(resource.isValid()).isFalse();\n        assertUpdateInvoked(resource, 0);\n\n        resource.setJSON(JSON_DATA);\n        assertThat(resource.getJSON()).isEqualTo(JSON_DATA);\n        assertThat(resource.isValid()).isTrue();\n        assertUpdateInvoked(resource, 0);\n\n        resource.setJSON(jsonData2);\n        assertThat(resource.getJSON()).isEqualTo(jsonData2);\n        assertThat(resource.isValid()).isTrue();\n        assertUpdateInvoked(resource, 0);\n    }\n\n    /**\n     * Test Retry-After\n     */\n    @Test\n    public void testRetryAfter() {\n        var login = TestUtils.login();\n        var retryAfter = Instant.now().plusSeconds(30L);\n        var jsonData = getJSON(\"requestOrderResponse\");\n\n        var resource = new DummyJsonResource(login, LOCATION_URL, jsonData, retryAfter);\n        assertThat(resource.isValid()).isTrue();\n        assertThat(resource.getJSON()).isEqualTo(jsonData);\n        assertThat(resource.getRetryAfter()).hasValue(retryAfter);\n        assertUpdateInvoked(resource, 0);\n\n        resource.setRetryAfter(null);\n        assertThat(resource.getRetryAfter()).isEmpty();\n    }\n\n    /**\n     * Test {@link AcmeJsonResource#invalidate()}.\n     */\n    @Test\n    public void testInvalidate() {\n        var login = TestUtils.login();\n\n        var resource = new DummyJsonResource(login, LOCATION_URL);\n        assertThat(resource.isValid()).isFalse();\n        assertUpdateInvoked(resource, 0);\n\n        resource.setJSON(JSON_DATA);\n        assertThat(resource.isValid()).isTrue();\n        assertUpdateInvoked(resource, 0);\n\n        resource.invalidate();\n        assertThat(resource.isValid()).isFalse();\n        assertUpdateInvoked(resource, 0);\n\n        assertThat(resource.getJSON()).isEqualTo(JSON_DATA);\n        assertThat(resource.isValid()).isTrue();\n        assertUpdateInvoked(resource, 1);\n    }\n\n    /**\n     * Assert that {@link AcmeJsonResource#update()} has been invoked a given number of\n     * times.\n     *\n     * @param resource\n     *            {@link AcmeJsonResource} to test\n     * @param count\n     *            Expected number of times\n     */\n    private static void assertUpdateInvoked(AcmeJsonResource resource, int count) {\n        var dummy = (DummyJsonResource) resource;\n        assertThat(dummy.updateCount).as(\"update counter\").isEqualTo(count);\n    }\n\n    /**\n     * Minimum implementation of {@link AcmeJsonResource}.\n     */\n    private static class DummyJsonResource extends AcmeJsonResource {\n        @Serial\n        private static final long serialVersionUID = -6459238185161771948L;\n\n        private int updateCount = 0;\n\n        public DummyJsonResource(Login login, URL location) {\n            super(login, location);\n        }\n\n        public DummyJsonResource(Login login, URL location, JSON json, @Nullable Instant retryAfter) {\n            super(login, location);\n            setJSON(json);\n            setRetryAfter(retryAfter);\n        }\n\n        @Override\n        public Optional<Instant> fetch() throws AcmeException {\n            // fetch() is tested individually in all AcmeJsonResource subclasses.\n            // Here we just simulate the update, by setting a JSON.\n            updateCount++;\n            setJSON(JSON_DATA);\n            return Optional.empty();\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/AcmeResourceTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\nimport java.io.Serial;\nimport java.net.URI;\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link AcmeResource}.\n */\npublic class AcmeResourceTest {\n\n    /**\n     * Test constructors and setters\n     */\n    @Test\n    public void testConstructor() throws Exception {\n        var login = TestUtils.login();\n        var location = URI.create(\"http://example.com/acme/resource\").toURL();\n\n        assertThrows(NullPointerException.class, () -> new DummyResource(null, null));\n\n        var resource = new DummyResource(login, location);\n        assertThat(resource.getLogin()).isEqualTo(login);\n        assertThat(resource.getLocation()).isEqualTo(location);\n    }\n\n    /**\n     * Test if {@link AcmeResource} is properly serialized.\n     */\n    @Test\n    public void testSerialization() throws Exception {\n        var login = TestUtils.login();\n        var location = URI.create(\"http://example.com/acme/resource\").toURL();\n\n        // Create a Challenge for testing\n        var challenge = new DummyResource(login, location);\n        assertThat(challenge.getLogin()).isEqualTo(login);\n\n        // Serialize it\n        byte[] serialized;\n        try (var baos = new ByteArrayOutputStream()) {\n            try (var out = new ObjectOutputStream(baos)) {\n                out.writeObject(challenge);\n            }\n            serialized = baos.toByteArray();\n        }\n\n        // Make sure there is no PrivateKey in the stream\n        var str = new String(serialized, StandardCharsets.ISO_8859_1);\n        assertThat(str).as(\"serialized stream contains a PrivateKey\")\n                .doesNotContain(\"Ljava/security/PrivateKey\");\n\n        // Deserialize to new object\n        DummyResource restored;\n        try (var bais = new ByteArrayInputStream(serialized);\n                var in = new ObjectInputStream(bais)) {\n            var obj = in.readObject();\n            assertThat(obj).isInstanceOf(DummyResource.class);\n            restored = (DummyResource) obj;\n        }\n        assertThat(restored).isNotSameAs(challenge);\n\n        // Make sure the restored object is not attached to a login\n        assertThrows(IllegalStateException.class, restored::getLogin);\n\n        // Rebind to login\n        restored.rebind(login);\n\n        // Make sure the new login is set\n        assertThat(restored.getLogin()).isEqualTo(login);\n    }\n\n    /**\n     * Test if a rebind attempt fails.\n     */\n    @Test\n    public void testRebind() {\n        assertThrows(IllegalStateException.class, () -> {\n            var login = TestUtils.login();\n            var location = URI.create(\"http://example.com/acme/resource\").toURL();\n\n            var resource = new DummyResource(login, location);\n            assertThat(resource.getLogin()).isEqualTo(login);\n\n            var login2 = TestUtils.login();\n            resource.rebind(login2); // fails to rebind to another login\n        });\n    }\n\n    /**\n     * Minimum implementation of {@link AcmeResource}.\n     */\n    private static class DummyResource extends AcmeResource {\n        @Serial\n        private static final long serialVersionUID = 7188822681353082472L;\n        public DummyResource(Login login, URL location) {\n            super(login, location);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/AuthorizationTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.within;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.io.IOException;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Optional;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.challenge.Challenge;\nimport org.shredzone.acme4j.challenge.Dns01Challenge;\nimport org.shredzone.acme4j.challenge.Http01Challenge;\nimport org.shredzone.acme4j.challenge.TlsAlpn01Challenge;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.provider.TestableConnectionProvider;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\n\n/**\n * Unit tests for {@link Authorization}.\n */\npublic class AuthorizationTest {\n\n    private static final String SNAILMAIL_TYPE = \"snail-01\"; // a non-existent challenge\n    private static final String DUPLICATE_TYPE = \"duplicate-01\"; // a duplicate challenge\n\n    private final URL locationUrl = url(\"http://example.com/acme/account\");\n\n    /**\n     * Test that {@link Authorization#findChallenge(String)} finds challenges.\n     */\n    @Test\n    public void testFindChallenge() throws IOException {\n        var authorization = createChallengeAuthorization();\n\n        // A snail mail challenge is not available at all\n        var c1 = authorization.findChallenge(SNAILMAIL_TYPE);\n        assertThat(c1).isEmpty();\n\n        // HttpChallenge is available\n        var c2 = authorization.findChallenge(Http01Challenge.TYPE);\n        assertThat(c2).isNotEmpty();\n        assertThat(c2.get()).isInstanceOf(Http01Challenge.class);\n\n        // Dns01Challenge is available\n        var c3 = authorization.findChallenge(Dns01Challenge.TYPE);\n        assertThat(c3).isNotEmpty();\n        assertThat(c3.get()).isInstanceOf(Dns01Challenge.class);\n\n        // TlsAlpn01Challenge is available\n        var c4 = authorization.findChallenge(TlsAlpn01Challenge.TYPE);\n        assertThat(c4).isNotEmpty();\n        assertThat(c4.get()).isInstanceOf(TlsAlpn01Challenge.class);\n    }\n\n    /**\n     * Test that {@link Authorization#findChallenge(Class)} finds challenges.\n     */\n    @Test\n    public void testFindChallengeByType() throws IOException {\n        var authorization = createChallengeAuthorization();\n\n        // A snail mail challenge is not available at all\n        var c1 = authorization.findChallenge(NonExistingChallenge.class);\n        assertThat(c1).isEmpty();\n\n        // HttpChallenge is available\n        var c2 = authorization.findChallenge(Http01Challenge.class);\n        assertThat(c2).isNotEmpty();\n\n        // Dns01Challenge is available\n        var c3 = authorization.findChallenge(Dns01Challenge.class);\n        assertThat(c3).isNotEmpty();\n\n        // TlsAlpn01Challenge is available\n        var c4 = authorization.findChallenge(TlsAlpn01Challenge.class);\n        assertThat(c4).isNotEmpty();\n    }\n\n    /**\n     * Test that {@link Authorization#findChallenge(String)} fails on duplicate\n     * challenges.\n     */\n    @Test\n    public void testFailDuplicateChallenges() {\n        assertThrows(AcmeProtocolException.class, () -> {\n            var authorization = createChallengeAuthorization();\n            authorization.findChallenge(DUPLICATE_TYPE);\n        });\n    }\n\n    /**\n     * Test that authorization is properly updated.\n     */\n    @Test\n    public void testUpdate() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"updateAuthorizationResponse\");\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putTestChallenge(\"http-01\", Http01Challenge::new);\n        provider.putTestChallenge(\"dns-01\", Dns01Challenge::new);\n        provider.putTestChallenge(\"tls-alpn-01\", TlsAlpn01Challenge::new);\n\n        var auth = new Authorization(login, locationUrl);\n        auth.fetch();\n\n        assertThat(auth.getIdentifier().getDomain()).isEqualTo(\"example.org\");\n        assertThat(auth.getStatus()).isEqualTo(Status.VALID);\n        assertThat(auth.isWildcard()).isFalse();\n        assertThat(auth.getExpires().orElseThrow()).isCloseTo(\"2016-01-02T17:12:40Z\", within(1, ChronoUnit.SECONDS));\n        assertThat(auth.getLocation()).isEqualTo(locationUrl);\n\n        assertThat(auth.getChallenges()).containsExactlyInAnyOrder(\n                        provider.getChallenge(Http01Challenge.TYPE),\n                        provider.getChallenge(Dns01Challenge.TYPE),\n                        provider.getChallenge(TlsAlpn01Challenge.TYPE));\n\n        provider.close();\n    }\n\n    /**\n     * Test that wildcard authorization are correct.\n     */\n    @Test\n    public void testWildcard() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"updateAuthorizationWildcardResponse\");\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putTestChallenge(\"dns-01\", Dns01Challenge::new);\n\n        var auth = new Authorization(login, locationUrl);\n        auth.fetch();\n\n        assertThat(auth.getIdentifier().getDomain()).isEqualTo(\"example.org\");\n        assertThat(auth.getStatus()).isEqualTo(Status.VALID);\n        assertThat(auth.isWildcard()).isTrue();\n        assertThat(auth.getExpires().orElseThrow()).isCloseTo(\"2016-01-02T17:12:40Z\", within(1, ChronoUnit.SECONDS));\n        assertThat(auth.getLocation()).isEqualTo(locationUrl);\n\n        assertThat(auth.getChallenges()).containsExactlyInAnyOrder(\n                        provider.getChallenge(Dns01Challenge.TYPE));\n\n        provider.close();\n    }\n\n    /**\n     * Test lazy loading.\n     */\n    @Test\n    public void testLazyLoading() throws Exception {\n        var requestWasSent = new AtomicBoolean(false);\n\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                requestWasSent.set(true);\n                assertThat(url).isEqualTo(locationUrl);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"updateAuthorizationResponse\");\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putTestChallenge(\"http-01\", Http01Challenge::new);\n        provider.putTestChallenge(\"dns-01\", Dns01Challenge::new);\n        provider.putTestChallenge(\"tls-alpn-01\", TlsAlpn01Challenge::new);\n\n        var auth = new Authorization(login, locationUrl);\n\n        // Lazy loading\n        assertThat(requestWasSent).isFalse();\n        assertThat(auth.getIdentifier().getDomain()).isEqualTo(\"example.org\");\n        assertThat(requestWasSent).isTrue();\n\n        // Subsequent queries do not trigger another load\n        requestWasSent.set(false);\n        assertThat(auth.getIdentifier().getDomain()).isEqualTo(\"example.org\");\n        assertThat(auth.getStatus()).isEqualTo(Status.VALID);\n        assertThat(auth.isWildcard()).isFalse();\n        assertThat(auth.getExpires().orElseThrow()).isCloseTo(\"2016-01-02T17:12:40Z\", within(1, ChronoUnit.SECONDS));\n        assertThat(requestWasSent).isFalse();\n\n        provider.close();\n    }\n\n    /**\n     * Test that authorization is properly updated, with retry-after header set.\n     */\n    @Test\n    public void testUpdateRetryAfter() throws Exception {\n        var retryAfter = Instant.now().plus(Duration.ofSeconds(30));\n\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"updateAuthorizationResponse\");\n            }\n\n            @Override\n            public Optional<Instant> getRetryAfter() {\n                return Optional.of(retryAfter);\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putTestChallenge(\"http-01\", Http01Challenge::new);\n        provider.putTestChallenge(\"dns-01\", Dns01Challenge::new);\n        provider.putTestChallenge(\"tls-alpn-01\", TlsAlpn01Challenge::new);\n\n        var auth = new Authorization(login, locationUrl);\n        var returnedRetryAfter = auth.fetch();\n        assertThat(returnedRetryAfter).hasValue(retryAfter);\n\n        assertThat(auth.getIdentifier().getDomain()).isEqualTo(\"example.org\");\n        assertThat(auth.getStatus()).isEqualTo(Status.VALID);\n        assertThat(auth.isWildcard()).isFalse();\n        assertThat(auth.getExpires().orElseThrow()).isCloseTo(\"2016-01-02T17:12:40Z\", within(1, ChronoUnit.SECONDS));\n        assertThat(auth.getLocation()).isEqualTo(locationUrl);\n\n        assertThat(auth.getChallenges()).containsExactlyInAnyOrder(\n                        provider.getChallenge(Http01Challenge.TYPE),\n                        provider.getChallenge(Dns01Challenge.TYPE),\n                        provider.getChallenge(TlsAlpn01Challenge.TYPE));\n\n        provider.close();\n    }\n\n    /**\n     * Test that an authorization can be deactivated.\n     */\n    @Test\n    public void testDeactivate() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                var json = claims.toJSON();\n                assertThat(json.get(\"status\").asString()).isEqualTo(\"deactivated\");\n                assertThat(url).isEqualTo(locationUrl);\n                assertThat(login).isNotNull();\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"updateAuthorizationResponse\");\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putTestChallenge(\"http-01\", Http01Challenge::new);\n        provider.putTestChallenge(\"dns-01\", Dns01Challenge::new);\n        provider.putTestChallenge(\"tls-alpn-01\", TlsAlpn01Challenge::new);\n\n        var auth = new Authorization(login, locationUrl);\n        auth.deactivate();\n\n        provider.close();\n    }\n\n    /**\n     * Creates an {@link Authorization} instance with a set of challenges.\n     */\n    private Authorization createChallengeAuthorization() throws IOException {\n        try (var provider = new TestableConnectionProvider()) {\n            var login = provider.createLogin();\n\n            provider.putTestChallenge(Http01Challenge.TYPE, Http01Challenge::new);\n            provider.putTestChallenge(Dns01Challenge.TYPE, Dns01Challenge::new);\n            provider.putTestChallenge(TlsAlpn01Challenge.TYPE, TlsAlpn01Challenge::new);\n            provider.putTestChallenge(DUPLICATE_TYPE, Challenge::new);\n\n            var authorization = new Authorization(login, locationUrl);\n            authorization.setJSON(getJSON(\"authorizationChallenges\"));\n            return authorization;\n        }\n    }\n\n    /**\n     * Dummy challenge that is never going to be created.\n     */\n    private static class NonExistingChallenge extends Challenge {\n        public NonExistingChallenge(Login login, JSON data) {\n            super(login, data);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.shredzone.acme4j.toolbox.TestUtils.*;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStreamWriter;\nimport java.net.HttpURLConnection;\nimport java.net.URI;\nimport java.net.URL;\nimport java.security.cert.X509Certificate;\nimport java.time.Instant;\nimport java.time.ZonedDateTime;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Optional;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.connector.RequestSigner;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.provider.TestableConnectionProvider;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link Certificate}.\n */\npublic class CertificateTest {\n\n    private final URL resourceUrl = url(\"http://example.com/acme/resource\");\n    private final URL locationUrl = url(\"http://example.com/acme/certificate\");\n    private final URL alternate1Url = url(\"https://example.com/acme/alt-cert/1\");\n    private final URL alternate2Url = url(\"https://example.com/acme/alt-cert/2\");\n\n    /**\n     * Test that a certificate can be downloaded.\n     */\n    @Test\n    public void testDownload() throws Exception {\n        var originalCert = TestUtils.createCertificate(\"/cert.pem\");\n        var alternateCert = TestUtils.createCertificate(\"/certid-cert.pem\");\n\n        var provider = new TestableConnectionProvider() {\n            List<X509Certificate> sendCert;\n\n            @Override\n            public int sendCertificateRequest(URL url, Login login) {\n                assertThat(url).isIn(locationUrl, alternate1Url, alternate2Url);\n                assertThat(login).isNotNull();\n                if (locationUrl.equals(url)) {\n                    sendCert = originalCert;\n                } else {\n                    sendCert = alternateCert;\n                }\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public List<X509Certificate> readCertificates() {\n                return sendCert;\n            }\n\n            @Override\n            public Collection<URL> getLinks(String relation) {\n                assertThat(relation).isEqualTo(\"alternate\");\n                return Arrays.asList(alternate1Url, alternate2Url);\n            }\n        };\n\n        var cert = new Certificate(provider.createLogin(), locationUrl);\n        cert.download();\n\n        var downloadedCert = cert.getCertificate();\n        assertThat(downloadedCert.getEncoded()).isEqualTo(originalCert.get(0).getEncoded());\n\n        var downloadedChain = cert.getCertificateChain();\n        assertThat(downloadedChain).hasSize(originalCert.size());\n        for (var ix = 0; ix < downloadedChain.size(); ix++) {\n            assertThat(downloadedChain.get(ix).getEncoded()).isEqualTo(originalCert.get(ix).getEncoded());\n        }\n\n        byte[] writtenPem;\n        byte[] originalPem;\n        try (var baos = new ByteArrayOutputStream(); var w = new OutputStreamWriter(baos)) {\n            cert.writeCertificate(w);\n            w.flush();\n            writtenPem = baos.toByteArray();\n        }\n        try (var baos = new ByteArrayOutputStream(); var in = getClass().getResourceAsStream(\"/cert.pem\")) {\n            int len;\n            var buffer = new byte[2048];\n            while((len = in.read(buffer)) >= 0) {\n                baos.write(buffer, 0, len);\n            }\n            originalPem = baos.toByteArray();\n        }\n        assertThat(writtenPem).isEqualTo(originalPem);\n\n        assertThat(cert.isIssuedBy(\"The ACME CA X1\")).isFalse();\n        assertThat(cert.isIssuedBy(CERT_ISSUER)).isTrue();\n\n        assertThat(cert.getAlternates()).isNotNull();\n        assertThat(cert.getAlternates()).hasSize(2);\n        assertThat(cert.getAlternates()).element(0).isEqualTo(alternate1Url);\n        assertThat(cert.getAlternates()).element(1).isEqualTo(alternate2Url);\n\n        assertThat(cert.getAlternateCertificates()).isNotNull();\n        assertThat(cert.getAlternateCertificates()).hasSize(2);\n        assertThat(cert.getAlternateCertificates())\n                .element(0)\n                .extracting(Certificate::getLocation)\n                .isEqualTo(alternate1Url);\n        assertThat(cert.getAlternateCertificates())\n                .element(1)\n                .extracting(Certificate::getLocation)\n                .isEqualTo(alternate2Url);\n\n        assertThat(cert.findCertificate(\"The ACME CA X1\")).\n                isEmpty();\n        assertThat(cert.findCertificate(CERT_ISSUER).orElseThrow())\n                .isSameAs(cert);\n        assertThat(cert.findCertificate(\"minica root ca 3a1356\").orElseThrow())\n                .isSameAs(cert.getAlternateCertificates().get(0));\n        assertThat(cert.getAlternateCertificates().get(0).isIssuedBy(\"minica root ca 3a1356\"))\n                .isTrue();\n\n        provider.close();\n    }\n\n    /**\n     * Test that a certificate can be revoked.\n     */\n    @Test\n    public void testRevokeCertificate() throws AcmeException, IOException {\n        var originalCert = TestUtils.createCertificate(\"/cert.pem\");\n\n        var provider = new TestableConnectionProvider() {\n            private boolean certRequested = false;\n\n            @Override\n            public int sendCertificateRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                assertThat(login).isNotNull();\n                certRequested = true;\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"revokeCertificateRequest\").toString());\n                assertThat(login).isNotNull();\n                certRequested = false;\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public List<X509Certificate> readCertificates() {\n                assertThat(certRequested).isTrue();\n                return originalCert;\n            }\n\n            @Override\n            public Collection<URL> getLinks(String relation) {\n                assertThat(relation).isEqualTo(\"alternate\");\n                return Collections.emptyList();\n            }\n        };\n\n        provider.putTestResource(Resource.REVOKE_CERT, resourceUrl);\n\n        var cert = new Certificate(provider.createLogin(), locationUrl);\n        cert.revoke();\n\n        provider.close();\n    }\n\n    /**\n     * Test that a certificate can be revoked with reason.\n     */\n    @Test\n    public void testRevokeCertificateWithReason() throws AcmeException, IOException {\n        var originalCert = TestUtils.createCertificate(\"/cert.pem\");\n\n        var provider = new TestableConnectionProvider() {\n            private boolean certRequested = false;\n\n            @Override\n            public int sendCertificateRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                assertThat(login).isNotNull();\n                certRequested = true;\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"revokeCertificateWithReasonRequest\").toString());\n                assertThat(login).isNotNull();\n                certRequested = false;\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public List<X509Certificate> readCertificates() {\n                assertThat(certRequested).isTrue();\n                return originalCert;\n            }\n\n            @Override\n            public Collection<URL> getLinks(String relation) {\n                assertThat(relation).isEqualTo(\"alternate\");\n                return Collections.emptyList();\n            }\n        };\n\n        provider.putTestResource(Resource.REVOKE_CERT, resourceUrl);\n\n        var cert = new Certificate(provider.createLogin(), locationUrl);\n        cert.revoke(RevocationReason.KEY_COMPROMISE);\n\n        provider.close();\n    }\n\n    /**\n     * Test that numeric revocation reasons are correctly translated.\n     */\n    @Test\n    public void testRevocationReason() {\n        assertThat(RevocationReason.code(1))\n                .isEqualTo(RevocationReason.KEY_COMPROMISE);\n    }\n\n    /**\n     * Test that a certificate can be revoked by its domain key pair.\n     */\n    @Test\n    public void testRevokeCertificateByKeyPair() throws AcmeException, IOException {\n        var originalCert = TestUtils.createCertificate(\"/cert.pem\");\n        var certKeyPair = TestUtils.createDomainKeyPair();\n\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Session session, RequestSigner signer) {\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"revokeCertificateWithReasonRequest\").toString());\n                assertThat(session).isNotNull();\n                return HttpURLConnection.HTTP_OK;\n            }\n        };\n\n        provider.putTestResource(Resource.REVOKE_CERT, resourceUrl);\n\n        var session = provider.createSession();\n\n        Certificate.revoke(session, certKeyPair, originalCert.get(0), RevocationReason.KEY_COMPROMISE);\n\n        provider.close();\n    }\n\n    /**\n     * Test that RenewalInfo is returned.\n     */\n    @Test\n    public void testRenewalInfo() throws AcmeException, IOException {\n        // certid-cert.pem and certId provided by ACME ARI specs and known good\n        var certId = \"aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE\";\n        var certIdCert = TestUtils.createCertificate(\"/certid-cert.pem\");\n        var certResourceUrl = URI.create(resourceUrl.toExternalForm() + \"/\" + certId).toURL();\n        var retryAfterInstant = Instant.now().plus(10L, ChronoUnit.DAYS);\n\n        var provider = new TestableConnectionProvider() {\n            private boolean certRequested = false;\n            private boolean infoRequested = false;\n\n            @Override\n            public int sendCertificateRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                assertThat(login).isNotNull();\n                certRequested = true;\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public int sendRequest(URL url, Session session, ZonedDateTime ifModifiedSince) {\n                assertThat(url).isEqualTo(certResourceUrl);\n                assertThat(session).isNotNull();\n                assertThat(ifModifiedSince).isNull();\n                infoRequested = true;\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                assertThat(infoRequested).isTrue();\n                return getJSON(\"renewalInfo\");\n            }\n\n            @Override\n            public List<X509Certificate> readCertificates() {\n                assertThat(certRequested).isTrue();\n                return certIdCert;\n            }\n\n            @Override\n            public Collection<URL> getLinks(String relation) {\n                return Collections.emptyList();\n            }\n\n            @Override\n            public Optional<Instant> getRetryAfter() {\n                return Optional.of(retryAfterInstant);\n            }\n        };\n\n        provider.putTestResource(Resource.RENEWAL_INFO, resourceUrl);\n\n        var cert = new Certificate(provider.createLogin(), locationUrl);\n        assertThat(cert.hasRenewalInfo()).isTrue();\n        assertThat(cert.getRenewalInfoLocation())\n                .hasValue(certResourceUrl);\n\n        var renewalInfo = cert.getRenewalInfo();\n        assertThat(renewalInfo.getRetryAfter())\n                .isEmpty();\n        assertThat(renewalInfo.getSuggestedWindowStart())\n                .isEqualTo(\"2021-01-03T00:00:00Z\");\n        assertThat(renewalInfo.getSuggestedWindowEnd())\n                .isEqualTo(\"2021-01-07T00:00:00Z\");\n        assertThat(renewalInfo.getExplanation())\n                .isNotEmpty()\n                .contains(url(\"https://example.com/docs/example-mass-reissuance-event\"));\n\n        assertThat(renewalInfo.fetch()).hasValue(retryAfterInstant);\n        assertThat(renewalInfo.getRetryAfter()).hasValue(retryAfterInstant);\n\n        provider.close();\n    }\n\n    /**\n     * Test that a certificate is marked as replaced.\n     */\n    @Test\n    public void testMarkedAsReplaced() throws AcmeException, IOException {\n        // certid-cert.pem and certId provided by ACME ARI specs and known good\n        var certId = \"aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE\";\n        var certIdCert = TestUtils.createCertificate(\"/certid-cert.pem\");\n        var certResourceUrl = URI.create(resourceUrl.toExternalForm() + \"/\" + certId).toURL();\n\n        var provider = new TestableConnectionProvider() {\n            private boolean certRequested = false;\n\n            @Override\n            public int sendCertificateRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                assertThat(login).isNotNull();\n                certRequested = true;\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(certRequested).isTrue();\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"replacedCertificateRequest\").toString());\n                assertThat(login).isNotNull();\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public List<X509Certificate> readCertificates() {\n                assertThat(certRequested).isTrue();\n                return certIdCert;\n            }\n\n            @Override\n            public Collection<URL> getLinks(String relation) {\n                return Collections.emptyList();\n            }\n        };\n\n        provider.putTestResource(Resource.RENEWAL_INFO, resourceUrl);\n\n        var cert = new Certificate(provider.createLogin(), locationUrl);\n        assertThat(cert.hasRenewalInfo()).isTrue();\n        assertThat(cert.getRenewalInfoLocation()).hasValue(certResourceUrl);\n\n        provider.close();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/IdentifierTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static org.assertj.core.api.Assertions.*;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.net.InetAddress;\nimport java.net.UnknownHostException;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\n\n/**\n * Unit tests for {@link Identifier}.\n */\npublic class IdentifierTest {\n\n    @Test\n    public void testConstants() {\n        assertThat(Identifier.TYPE_DNS).isEqualTo(\"dns\");\n        assertThat(Identifier.TYPE_IP).isEqualTo(\"ip\");\n    }\n\n    @Test\n    public void testGetters() {\n        var id1 = new Identifier(\"foo\", \"123.456\");\n        assertThat(id1.getType()).isEqualTo(\"foo\");\n        assertThat(id1.getValue()).isEqualTo(\"123.456\");\n        assertThat(id1.toString()).isEqualTo(\"foo=123.456\");\n        var map1 = id1.toMap();\n        assertThat(map1).hasSize(2);\n        assertThat(map1.get(\"type\")).isEqualTo(\"foo\");\n        assertThat(map1.get(\"value\")).isEqualTo(\"123.456\");\n\n        var jb = new JSONBuilder();\n        jb.put(\"type\", \"bar\");\n        jb.put(\"value\", \"654.321\");\n        var id2 = new Identifier(jb.toJSON());\n        assertThat(id2.getType()).isEqualTo(\"bar\");\n        assertThat(id2.getValue()).isEqualTo(\"654.321\");\n        assertThat(id2.toString()).isEqualTo(\"bar=654.321\");\n        var map2 = id2.toMap();\n        assertThat(map2).hasSize(2);\n        assertThat(map2.get(\"type\")).isEqualTo(\"bar\");\n        assertThat(map2.get(\"value\")).isEqualTo(\"654.321\");\n    }\n\n    @Test\n    public void testDns() {\n        var id1 = Identifier.dns(\"example.com\");\n        assertThat(id1.getType()).isEqualTo(Identifier.TYPE_DNS);\n        assertThat(id1.getValue()).isEqualTo(\"example.com\");\n        assertThat(id1.getDomain()).isEqualTo(\"example.com\");\n\n        var id2 = Identifier.dns(\"ëxämþlë.com\");\n        assertThat(id2.getType()).isEqualTo(Identifier.TYPE_DNS);\n        assertThat(id2.getValue()).isEqualTo(\"xn--xml-qla7ae5k.com\");\n        assertThat(id2.getDomain()).isEqualTo(\"xn--xml-qla7ae5k.com\");\n    }\n\n    @Test\n    public void testNoDns() {\n        assertThrows(AcmeProtocolException.class, () ->\n            new Identifier(\"foo\", \"example.com\").getDomain()\n        );\n    }\n\n    @Test\n    public void testIp() throws UnknownHostException {\n        var id1 = Identifier.ip(InetAddress.getByName(\"192.0.2.2\"));\n        assertThat(id1.getType()).isEqualTo(Identifier.TYPE_IP);\n        assertThat(id1.getValue()).isEqualTo(\"192.0.2.2\");\n        assertThat(id1.getIP().getHostAddress()).isEqualTo(\"192.0.2.2\");\n\n        var id2 = Identifier.ip(InetAddress.getByName(\"2001:db8:85a3::8a2e:370:7334\"));\n        assertThat(id2.getType()).isEqualTo(Identifier.TYPE_IP);\n        assertThat(id2.getValue()).isEqualTo(\"2001:db8:85a3:0:0:8a2e:370:7334\");\n        assertThat(id2.getIP().getHostAddress()).isEqualTo(\"2001:db8:85a3:0:0:8a2e:370:7334\");\n\n        var id3 = Identifier.ip(\"192.0.2.99\");\n        assertThat(id3.getType()).isEqualTo(Identifier.TYPE_IP);\n        assertThat(id3.getValue()).isEqualTo(\"192.0.2.99\");\n        assertThat(id3.getIP().getHostAddress()).isEqualTo(\"192.0.2.99\");\n    }\n\n    @Test\n    public void testNoIp() {\n        assertThrows(AcmeProtocolException.class, () ->\n            new Identifier(\"foo\", \"example.com\").getIP()\n        );\n    }\n\n    @Test\n    public void testAncestorDomain() {\n        var id1 = Identifier.dns(\"foo.bar.example.com\");\n        var id1a = id1.withAncestorDomain(\"example.com\");\n        assertThat(id1a).isNotSameAs(id1);\n        assertThat(id1a.getType()).isEqualTo(Identifier.TYPE_DNS);\n        assertThat(id1a.getValue()).isEqualTo(\"foo.bar.example.com\");\n        assertThat(id1a.getDomain()).isEqualTo(\"foo.bar.example.com\");\n        assertThat(id1a.toMap()).contains(\n                entry(\"type\", \"dns\"),\n                entry(\"value\", \"foo.bar.example.com\"),\n                entry(\"ancestorDomain\", \"example.com\")\n        );\n        assertThat(id1a.toString()).isEqualTo(\"{ancestorDomain=example.com, type=dns, value=foo.bar.example.com}\");\n\n        var id2 = Identifier.dns(\"föö.ëxämþlë.com\").withAncestorDomain(\"ëxämþlë.com\");\n        assertThat(id2.getType()).isEqualTo(Identifier.TYPE_DNS);\n        assertThat(id2.getValue()).isEqualTo(\"xn--f-1gaa.xn--xml-qla7ae5k.com\");\n        assertThat(id2.getDomain()).isEqualTo(\"xn--f-1gaa.xn--xml-qla7ae5k.com\");\n        assertThat(id2.toMap()).contains(\n                entry(\"type\", \"dns\"),\n                entry(\"value\", \"xn--f-1gaa.xn--xml-qla7ae5k.com\"),\n                entry(\"ancestorDomain\", \"xn--xml-qla7ae5k.com\")\n        );\n\n        var id3 = Identifier.dns(\"foo.bar.example.com\").withAncestorDomain(\"example.com\");\n        assertThat(id3.equals(id1)).isFalse();\n        assertThat(id3.equals(id1a)).isTrue();\n\n        assertThatExceptionOfType(AcmeProtocolException.class).isThrownBy(() ->\n                Identifier.ip(\"192.0.2.99\").withAncestorDomain(\"example.com\")\n        );\n\n        assertThatNullPointerException().isThrownBy(() ->\n                Identifier.dns(\"example.org\").withAncestorDomain(null)\n        );\n    }\n\n    @Test\n    public void testAllowSubdomainAuth() {\n        var id1 = Identifier.dns(\"example.com\");\n        var id1a = id1.allowSubdomainAuth();\n        assertThat(id1a).isNotSameAs(id1);\n        assertThat(id1a.getType()).isEqualTo(Identifier.TYPE_DNS);\n        assertThat(id1a.getValue()).isEqualTo(\"example.com\");\n        assertThat(id1a.getDomain()).isEqualTo(\"example.com\");\n        assertThat(id1a.toMap()).contains(\n                entry(\"type\", \"dns\"),\n                entry(\"value\", \"example.com\"),\n                entry(\"subdomainAuthAllowed\", true)\n        );\n        assertThat(id1a.toString()).isEqualTo(\"{subdomainAuthAllowed=true, type=dns, value=example.com}\");\n\n        var id3 = Identifier.dns(\"example.com\").allowSubdomainAuth();\n        assertThat(id3.equals(id1)).isFalse();\n        assertThat(id3.equals(id1a)).isTrue();\n\n        assertThatExceptionOfType(AcmeProtocolException.class).isThrownBy(() ->\n                Identifier.ip(\"192.0.2.99\").allowSubdomainAuth()\n        );\n    }\n\n    @Test\n    public void testEquals() {\n        var idRef = new Identifier(\"foo\", \"123.456\");\n\n        var id1 = new Identifier(\"foo\", \"123.456\");\n        assertThat(idRef.equals(id1)).isTrue();\n        assertThat(id1.equals(idRef)).isTrue();\n\n        var id2 = new Identifier(\"bar\", \"654.321\");\n        assertThat(idRef.equals(id2)).isFalse();\n\n        var id3 = new Identifier(\"foo\", \"555.666\");\n        assertThat(idRef.equals(id3)).isFalse();\n\n        var id4 = new Identifier(\"sna\", \"123.456\");\n        assertThat(idRef.equals(id4)).isFalse();\n\n        assertThat(idRef.equals(new Object())).isFalse();\n        assertThat(idRef.equals(null)).isFalse();\n    }\n\n    @Test\n    public void testNull() {\n        assertThrows(NullPointerException.class, () -> new Identifier(null, \"123.456\"));\n        assertThrows(NullPointerException.class, () -> new Identifier(\"foo\", null));\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/LoginTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.Mockito.*;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.io.IOException;\nimport java.net.HttpURLConnection;\nimport java.net.URI;\nimport java.net.URL;\n\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentMatchers;\nimport org.shredzone.acme4j.challenge.Challenge;\nimport org.shredzone.acme4j.challenge.Dns01Challenge;\nimport org.shredzone.acme4j.challenge.Http01Challenge;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.provider.AcmeProvider;\nimport org.shredzone.acme4j.provider.TestableConnectionProvider;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link Login}.\n */\npublic class LoginTest {\n\n    private final URL resourceUrl = url(\"https://example.com/acme/resource/123\");\n\n    /**\n     * Test the constructor.\n     */\n    @Test\n    public void testConstructor() throws IOException {\n        var location = url(TestUtils.ACCOUNT_URL);\n        var keypair = TestUtils.createKeyPair();\n        var session = TestUtils.session();\n\n        var login = new Login(location, keypair, session);\n        assertThat(login.getAccount().getLocation()).isEqualTo(location);\n        assertThat(login.getPublicKey()).isEqualTo(keypair.getPublic());\n        assertThat(login.getSession()).isEqualTo(session);\n\n        assertThat(login.getAccount()).isNotNull();\n        assertThat(login.getAccount().getLogin()).isEqualTo(login);\n        assertThat(login.getAccount().getLocation()).isEqualTo(location);\n        assertThat(login.getAccount().getSession()).isEqualTo(session);\n    }\n\n    /**\n     * Test the simple binders.\n     */\n    @Test\n    public void testBinder() throws IOException {\n        var location = url(TestUtils.ACCOUNT_URL);\n        var keypair = TestUtils.createKeyPair();\n        var session = TestUtils.session();\n\n        var login = new Login(location, keypair, session);\n\n        var auth = login.bindAuthorization(resourceUrl);\n        assertThat(auth).isNotNull();\n        assertThat(auth.getLogin()).isEqualTo(login);\n        assertThat(auth.getLocation()).isEqualTo(resourceUrl);\n\n        var cert = login.bindCertificate(resourceUrl);\n        assertThat(cert).isNotNull();\n        assertThat(cert.getLogin()).isEqualTo(login);\n        assertThat(cert.getLocation()).isEqualTo(resourceUrl);\n\n        var order = login.bindOrder(resourceUrl);\n        assertThat(order).isNotNull();\n        assertThat(order.getLogin()).isEqualTo(login);\n        assertThat(order.getLocation()).isEqualTo(resourceUrl);\n    }\n\n    /**\n     * Test that the account's keypair can be changed.\n     */\n    @Test\n    public void testKeyChange() throws IOException {\n        var location = url(TestUtils.ACCOUNT_URL);\n        var keypair = TestUtils.createKeyPair();\n        var session = TestUtils.session();\n\n        var login = new Login(location, keypair, session);\n        assertThat(login.getPublicKey()).isEqualTo(keypair.getPublic());\n\n        var keypair2 = TestUtils.createKeyPair();\n        login.setKeyPair(keypair2);\n        assertThat(login.getPublicKey()).isEqualTo(keypair2.getPublic());\n    }\n\n    /**\n     * Test that challenges are correctly created via provider.\n     */\n    @Test\n    public void testCreateChallenge() throws Exception {\n        var challengeType = Http01Challenge.TYPE;\n        var challengeUrl = url(\"https://example.com/acme/authz/0\");\n\n        var data = new JSONBuilder()\n                        .put(\"type\", challengeType)\n                        .put(\"url\", challengeUrl)\n                        .toJSON();\n\n        var mockChallenge = mock(Http01Challenge.class);\n        var mockProvider = mock(AcmeProvider.class);\n\n        when(mockProvider.createChallenge(\n                        ArgumentMatchers.any(Login.class),\n                        ArgumentMatchers.eq(data)))\n                .thenReturn(mockChallenge);\n\n        var location = url(TestUtils.ACCOUNT_URL);\n        var keypair = TestUtils.createKeyPair();\n        var session = TestUtils.session(mockProvider);\n\n        var login = new Login(location, keypair, session);\n        var challenge = login.createChallenge(data);\n        assertThat(challenge).isInstanceOf(Http01Challenge.class);\n        assertThat(challenge).isSameAs(mockChallenge);\n\n        verify(mockProvider).createChallenge(login, data);\n    }\n\n    /**\n     * Test that binding to a challenge invokes createChallenge\n     */\n    @Test\n    public void testBindChallenge() throws Exception {\n        var locationUrl = URI.create(\"https://example.com/acme/challenge/1\").toURL();\n\n        var mockChallenge = mock(Http01Challenge.class);\n        when(mockChallenge.getType()).thenReturn(Http01Challenge.TYPE);\n        var httpChallenge = getJSON(\"httpChallenge\");\n        var provider  = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return httpChallenge;\n            }\n\n            @Override\n            public Challenge createChallenge(Login login, JSON json) {\n                assertThat(json).isEqualTo(httpChallenge);\n                return mockChallenge;\n            }\n        };\n\n        var login = provider.createLogin();\n        var challenge = login.bindChallenge(locationUrl);\n        assertThat(challenge).isInstanceOf(Http01Challenge.class);\n        assertThat(challenge).isSameAs(mockChallenge);\n\n        var challenge2 = login.bindChallenge(locationUrl, Http01Challenge.class);\n        assertThat(challenge2).isSameAs(mockChallenge);\n\n        var ex = assertThrows(AcmeProtocolException.class,\n                () -> login.bindChallenge(locationUrl, Dns01Challenge.class));\n        assertThat(ex.getMessage()).isEqualTo(\"Challenge type http-01 does not match\" +\n                \" requested class class org.shredzone.acme4j.challenge.Dns01Challenge\");\n    }\n\n    /**\n     * Test that a new order can be created.\n     */\n    @Test\n    public void testNewOrder() throws AcmeException, IOException {\n        var provider = new TestableConnectionProvider();\n        var login = provider.createLogin();\n\n        assertThat(login.newOrder()).isNotNull();\n\n        provider.close();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/OrderBuilderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.*;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.io.IOException;\nimport java.net.HttpURLConnection;\nimport java.net.InetAddress;\nimport java.net.URL;\nimport java.time.Duration;\nimport java.util.Arrays;\n\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.exception.AcmeNotSupportedException;\nimport org.shredzone.acme4j.provider.TestableConnectionProvider;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link OrderBuilder}.\n */\npublic class OrderBuilderTest {\n\n    private final URL resourceUrl  = url(\"http://example.com/acme/resource\");\n    private final URL locationUrl  = url(TestUtils.ACCOUNT_URL);\n\n    /**\n     * Test that a new {@link Order} can be created.\n     */\n    @Test\n    public void testOrderCertificate() throws Exception {\n        var notBefore = parseTimestamp(\"2016-01-01T00:00:00Z\");\n        var notAfter = parseTimestamp(\"2016-01-08T00:00:00Z\");\n\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"requestOrderRequest\").toString());\n                assertThat(login).isNotNull();\n                return HttpURLConnection.HTTP_CREATED;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"requestOrderResponse\");\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);\n\n        var account = new Account(login, locationUrl);\n        var order = account.newOrder()\n                        .domains(\"example.com\", \"www.example.com\")\n                        .domain(\"example.org\")\n                        .domains(Arrays.asList(\"m.example.com\", \"m.example.org\"))\n                        .identifier(Identifier.dns(\"d.example.com\"))\n                        .identifiers(Arrays.asList(\n                                    Identifier.dns(\"d2.example.com\"),\n                                    Identifier.ip(InetAddress.getByName(\"192.0.2.2\"))))\n                        .notBefore(notBefore)\n                        .notAfter(notAfter)\n                        .create();\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder(\n                        Identifier.dns(\"example.com\"),\n                        Identifier.dns(\"www.example.com\"),\n                        Identifier.dns(\"example.org\"),\n                        Identifier.dns(\"m.example.com\"),\n                        Identifier.dns(\"m.example.org\"),\n                        Identifier.dns(\"d.example.com\"),\n                        Identifier.dns(\"d2.example.com\"),\n                        Identifier.ip(InetAddress.getByName(\"192.0.2.2\")));\n            softly.assertThat(order.getNotBefore().orElseThrow())\n                    .isEqualTo(\"2016-01-01T00:10:00Z\");\n            softly.assertThat(order.getNotAfter().orElseThrow())\n                    .isEqualTo(\"2016-01-08T00:10:00Z\");\n            softly.assertThat(order.getExpires().orElseThrow())\n                    .isEqualTo(\"2016-01-10T00:00:00Z\");\n            softly.assertThat(order.getStatus()).isEqualTo(Status.PENDING);\n            softly.assertThat(order.isAutoRenewing()).isFalse();\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(order::getAutoRenewalStartDate);\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(order::getAutoRenewalEndDate);\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(order::getAutoRenewalLifetime);\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(order::getAutoRenewalLifetimeAdjust);\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(order::isAutoRenewalGetEnabled);\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(order::getProfile);\n            softly.assertThat(order.getLocation()).isEqualTo(locationUrl);\n            softly.assertThat(order.getAuthorizations()).isNotNull();\n            softly.assertThat(order.getAuthorizations()).hasSize(2);\n        }\n\n        provider.close();\n    }\n\n    /**\n     * Test that a new auto-renewal {@link Order} can be created.\n     */\n    @Test\n    public void testAutoRenewOrderCertificate() throws Exception {\n        var autoRenewStart = parseTimestamp(\"2018-01-01T00:00:00Z\");\n        var autoRenewEnd = parseTimestamp(\"2019-01-01T00:00:00Z\");\n        var validity = Duration.ofDays(7);\n        var predate = Duration.ofDays(6);\n\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"requestAutoRenewOrderRequest\").toString());\n                assertThat(login).isNotNull();\n                return HttpURLConnection.HTTP_CREATED;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"requestAutoRenewOrderResponse\");\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putMetadata(\"auto-renewal\",JSON.parse(\n                \"{\\\"allow-certificate-get\\\": true}\"\n        ).toMap());\n        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);\n\n        var account = new Account(login, locationUrl);\n        var order = account.newOrder()\n                        .domain(\"example.org\")\n                        .autoRenewal()\n                        .autoRenewalStart(autoRenewStart)\n                        .autoRenewalEnd(autoRenewEnd)\n                        .autoRenewalLifetime(validity)\n                        .autoRenewalLifetimeAdjust(predate)\n                        .autoRenewalEnableGet()\n                        .create();\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder(Identifier.dns(\"example.org\"));\n            softly.assertThat(order.getNotBefore()).isEmpty();\n            softly.assertThat(order.getNotAfter()).isEmpty();\n            softly.assertThat(order.isAutoRenewing()).isTrue();\n            softly.assertThat(order.getAutoRenewalStartDate().orElseThrow()).isEqualTo(autoRenewStart);\n            softly.assertThat(order.getAutoRenewalEndDate()).isEqualTo(autoRenewEnd);\n            softly.assertThat(order.getAutoRenewalLifetime()).isEqualTo(validity);\n            softly.assertThat(order.getAutoRenewalLifetimeAdjust().orElseThrow()).isEqualTo(predate);\n            softly.assertThat(order.isAutoRenewalGetEnabled()).isTrue();\n            softly.assertThat(order.getLocation()).isEqualTo(locationUrl);\n        }\n\n        provider.close();\n    }\n\n    /**\n     * Test that a new {@link Order} with ancestor domain can be created.\n     */\n    @Test\n    public void testOrderCertificateWithAncestor() throws Exception {\n        var notBefore = parseTimestamp(\"2016-01-01T00:00:00Z\");\n        var notAfter = parseTimestamp(\"2016-01-08T00:00:00Z\");\n\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"requestOrderRequestSub\").toString());\n                assertThat(login).isNotNull();\n                return HttpURLConnection.HTTP_CREATED;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"requestOrderResponseSub\");\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);\n        provider.putMetadata(\"subdomainAuthAllowed\", true);\n\n        var account = new Account(login, locationUrl);\n        var order = account.newOrder()\n                .identifier(Identifier.dns(\"foo.bar.example.com\").withAncestorDomain(\"example.com\"))\n                .notBefore(notBefore)\n                .notAfter(notAfter)\n                .create();\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder(\n                    Identifier.dns(\"foo.bar.example.com\"));\n            softly.assertThat(order.getNotBefore().orElseThrow())\n                    .isEqualTo(\"2016-01-01T00:10:00Z\");\n            softly.assertThat(order.getNotAfter().orElseThrow())\n                    .isEqualTo(\"2016-01-08T00:10:00Z\");\n            softly.assertThat(order.getExpires().orElseThrow())\n                    .isEqualTo(\"2016-01-10T00:00:00Z\");\n            softly.assertThat(order.getStatus()).isEqualTo(Status.PENDING);\n            softly.assertThat(order.getLocation()).isEqualTo(locationUrl);\n            softly.assertThat(order.getAuthorizations()).isNotNull();\n            softly.assertThat(order.getAuthorizations()).hasSize(2);\n        }\n\n        provider.close();\n    }\n\n    /**\n     * Test that a new {@link Order} with ancestor domain fails if not supported.\n     */\n    @Test\n    public void testOrderCertificateWithAncestorFails() throws Exception {\n        var provider = new TestableConnectionProvider();\n\n        var login = provider.createLogin();\n\n        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);\n\n        assertThat(login.getSession().getMetadata().isSubdomainAuthAllowed()).isFalse();\n\n        var account = new Account(login, locationUrl);\n        assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() ->\n                account.newOrder()\n                        .identifier(Identifier.dns(\"foo.bar.example.com\").withAncestorDomain(\"example.com\"))\n                        .create()\n        );\n\n        provider.close();\n    }\n\n    /**\n     * Test that an auto-renewal {@link Order} cannot be created if unsupported by the CA.\n     */\n    @Test\n    public void testAutoRenewOrderCertificateFails() {\n        assertThrows(AcmeNotSupportedException.class, () -> {\n            var provider = new TestableConnectionProvider();\n            provider.putTestResource(Resource.NEW_ORDER, resourceUrl);\n\n            var login = provider.createLogin();\n\n            var account = new Account(login, locationUrl);\n            account.newOrder()\n                            .domain(\"example.org\")\n                            .autoRenewal()\n                            .create();\n\n            provider.close();\n        });\n    }\n\n    /**\n     * Test that auto-renew and notBefore/notAfter cannot be mixed.\n     */\n    @Test\n    public void testAutoRenewNotMixed() throws Exception {\n        var someInstant = parseTimestamp(\"2018-01-01T00:00:00Z\");\n\n        var provider = new TestableConnectionProvider();\n        var login = provider.createLogin();\n\n        var account = new Account(login, locationUrl);\n\n        assertThrows(IllegalArgumentException.class, () -> {\n            OrderBuilder ob = account.newOrder().autoRenewal();\n            ob.notBefore(someInstant);\n        }, \"accepted notBefore\");\n\n        assertThrows(IllegalArgumentException.class, () -> {\n            OrderBuilder ob = account.newOrder().autoRenewal();\n            ob.notAfter(someInstant);\n        }, \"accepted notAfter\");\n\n        assertThrows(IllegalArgumentException.class, () -> {\n            OrderBuilder ob = account.newOrder().notBefore(someInstant);\n            ob.autoRenewal();\n        }, \"accepted autoRenewal\");\n\n        assertThrows(IllegalArgumentException.class, () -> {\n            OrderBuilder ob = account.newOrder().notBefore(someInstant);\n            ob.autoRenewalStart(someInstant);\n        }, \"accepted autoRenewalStart\");\n\n        assertThrows(IllegalArgumentException.class, () -> {\n            OrderBuilder ob = account.newOrder().notBefore(someInstant);\n            ob.autoRenewalEnd(someInstant);\n        }, \"accepted autoRenewalEnd\");\n\n        assertThrows(IllegalArgumentException.class, () -> {\n            OrderBuilder ob = account.newOrder().notBefore(someInstant);\n            ob.autoRenewalLifetime(Duration.ofDays(7));\n        }, \"accepted autoRenewalLifetime\");\n\n        provider.close();\n    }\n\n    /**\n     * Test that a new profile {@link Order} can be created.\n     */\n    @Test\n    public void testProfileOrderCertificate() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"requestProfileOrderRequest\").toString());\n                assertThat(login).isNotNull();\n                return HttpURLConnection.HTTP_CREATED;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"requestProfileOrderResponse\");\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putMetadata(\"profiles\",JSON.parse(\n                \"{\\\"classic\\\": \\\"The same profile you're accustomed to\\\"}\"\n        ).toMap());\n        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);\n\n        var account = new Account(login, locationUrl);\n        var order = account.newOrder()\n                .domain(\"example.org\")\n                .profile(\"classic\")\n                .create();\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(order.getProfile()).isEqualTo(\"classic\");\n        }\n\n        provider.close();\n    }\n\n    /**\n     * Test that a profile {@link Order} cannot be created if the profile is unsupported\n     * by the CA.\n     */\n    @Test\n    public void testUnsupportedProfileOrderCertificateFails() throws Exception {\n        var provider = new TestableConnectionProvider();\n        provider.putMetadata(\"profiles\",JSON.parse(\n                \"{\\\"classic\\\": \\\"The same profile you're accustomed to\\\"}\"\n        ).toMap());\n        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);\n\n        var login = provider.createLogin();\n\n        var account = new Account(login, locationUrl);\n        assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() -> {\n            account.newOrder()\n                    .domain(\"example.org\")\n                    .profile(\"invalid\")\n                    .create();\n        }).withMessage(\"Server does not support profile: invalid\");\n        provider.close();\n    }\n\n    /**\n     * Test that a profile {@link Order} cannot be created if the feature is unsupported\n     * by the CA.\n     */\n    @Test\n    public void testProfileOrderCertificateFails() throws IOException {\n        var provider = new TestableConnectionProvider();\n        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);\n\n        var login = provider.createLogin();\n\n        var account = new Account(login, locationUrl);\n        assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() -> {\n            account.newOrder()\n                    .domain(\"example.org\")\n                    .profile(\"classic\")\n                    .create();\n        }).withMessage(\"Server does not support profile\");\n\n        provider.close();\n    }\n\n    /**\n     * Test that the ARI replaces field is set.\n     */\n    @Test\n    public void testARIReplaces() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(url).isEqualTo(resourceUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"requestReplacesRequest\").toString());\n                assertThat(login).isNotNull();\n                return HttpURLConnection.HTTP_CREATED;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"requestReplacesResponse\");\n            }\n\n            @Override\n            public URL getLocation() {\n                return locationUrl;\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);\n        provider.putTestResource(Resource.RENEWAL_INFO, resourceUrl);\n\n        var account = new Account(login, locationUrl);\n        account.newOrder()\n                .domain(\"example.org\")\n                .replaces(\"aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE\")\n                .create();\n\n        provider.close();\n    }\n\n    /**\n     * Test that exception is thrown if the ARI replaces field is set but ARI is not\n     * supported.\n     */\n    @Test\n    public void testARIReplaceFails() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                fail(\"Request was sent\");\n                return HttpURLConnection.HTTP_FORBIDDEN;\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);\n\n        var account = new Account(login, locationUrl);\n        assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() -> {\n                    account.newOrder()\n                            .domain(\"example.org\")\n                            .replaces(\"aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE\")\n                            .create();\n                })\n                .withMessage(\"Server does not support renewal-information\");\n\n        provider.close();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.net.HttpURLConnection;\nimport java.net.URI;\nimport java.net.URL;\nimport java.time.Duration;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.exception.AcmeNotSupportedException;\nimport org.shredzone.acme4j.provider.TestableConnectionProvider;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link Order}.\n */\npublic class OrderTest {\n\n    private final URL locationUrl = url(\"http://example.com/acme/order/1234\");\n    private final URL finalizeUrl = url(\"https://example.com/acme/acct/1/order/1/finalize\");\n\n    /**\n     * Test that order is properly updated.\n     */\n    @Test\n    public void testUpdate() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"updateOrderResponse\");\n            }\n        };\n\n        var login = provider.createLogin();\n\n        var order = new Order(login, locationUrl);\n        order.fetch();\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(order.getStatus()).isEqualTo(Status.PENDING);\n            softly.assertThat(order.getExpires().orElseThrow()).isEqualTo(\"2015-03-01T14:09:00Z\");\n            softly.assertThat(order.getLocation()).isEqualTo(locationUrl);\n\n            softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder(\n                    Identifier.dns(\"example.com\"),\n                    Identifier.dns(\"www.example.com\"));\n            softly.assertThat(order.getNotBefore().orElseThrow())\n                    .isEqualTo(\"2016-01-01T00:00:00Z\");\n            softly.assertThat(order.getNotAfter().orElseThrow())\n                    .isEqualTo(\"2016-01-08T00:00:00Z\");\n            softly.assertThat(order.getCertificate().getLocation())\n                    .isEqualTo(url(\"https://example.com/acme/cert/1234\"));\n            softly.assertThat(order.getFinalizeLocation()).isEqualTo(finalizeUrl);\n\n            softly.assertThat(order.isAutoRenewing()).isFalse();\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(order::getAutoRenewalStartDate);\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(order::getAutoRenewalEndDate);\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(order::getAutoRenewalLifetime);\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(order::getAutoRenewalLifetimeAdjust);\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(order::isAutoRenewalGetEnabled);\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(order::getProfile);\n\n            softly.assertThat(order.getError()).isNotEmpty();\n            softly.assertThat(order.getError().orElseThrow().getType())\n                    .isEqualTo(URI.create(\"urn:ietf:params:acme:error:connection\"));\n            softly.assertThat(order.getError().flatMap(Problem::getDetail).orElseThrow())\n                    .isEqualTo(\"connection refused\");\n\n            var auths = order.getAuthorizations();\n            softly.assertThat(auths).hasSize(2);\n            softly.assertThat(auths.stream())\n                    .map(Authorization::getLocation)\n                    .containsExactlyInAnyOrder(\n                            url(\"https://example.com/acme/authz/1234\"),\n                            url(\"https://example.com/acme/authz/2345\"));\n        }\n\n        provider.close();\n    }\n\n    /**\n     * Test lazy loading.\n     */\n    @Test\n    public void testLazyLoading() throws Exception {\n        var requestWasSent = new AtomicBoolean(false);\n\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                requestWasSent.set(true);\n                assertThat(url).isEqualTo(locationUrl);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"updateOrderResponse\");\n            }\n        };\n\n        var login = provider.createLogin();\n\n        var order = new Order(login, locationUrl);\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            // Lazy loading\n            softly.assertThat(requestWasSent).isFalse();\n            softly.assertThat(order.getCertificate().getLocation())\n                    .isEqualTo(url(\"https://example.com/acme/cert/1234\"));\n            softly.assertThat(requestWasSent).isTrue();\n\n            // Subsequent queries do not trigger another load\n            requestWasSent.set(false);\n            softly.assertThat(order.getCertificate().getLocation())\n                    .isEqualTo(url(\"https://example.com/acme/cert/1234\"));\n            softly.assertThat(order.getStatus()).isEqualTo(Status.PENDING);\n            softly.assertThat(order.getExpires().orElseThrow()).isEqualTo(\"2015-03-01T14:09:00Z\");\n            softly.assertThat(requestWasSent).isFalse();\n        }\n\n        provider.close();\n    }\n\n    /**\n     * Test that order is properly finalized.\n     */\n    @Test\n    public void testFinalize() throws Exception {\n        var csr = TestUtils.getResourceAsByteArray(\"/csr.der\");\n\n        var provider = new TestableConnectionProvider() {\n            private boolean isFinalized = false;\n\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(url).isEqualTo(finalizeUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"finalizeRequest\").toString());\n                assertThat(login).isNotNull();\n                isFinalized = true;\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(isFinalized ? \"finalizeResponse\" : \"updateOrderResponse\");\n            }\n        };\n\n        var login = provider.createLogin();\n\n        var order = new Order(login, locationUrl);\n        order.execute(csr);\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(order.getStatus()).isEqualTo(Status.VALID);\n            softly.assertThat(order.getExpires().orElseThrow()).isEqualTo(\"2015-03-01T14:09:00Z\");\n            softly.assertThat(order.getLocation()).isEqualTo(locationUrl);\n\n            softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder(\n                    Identifier.dns(\"example.com\"),\n                    Identifier.dns(\"www.example.com\"));\n            softly.assertThat(order.getNotBefore().orElseThrow())\n                    .isEqualTo(\"2016-01-01T00:00:00Z\");\n            softly.assertThat(order.getNotAfter().orElseThrow())\n                    .isEqualTo(\"2016-01-08T00:00:00Z\");\n            softly.assertThat(order.isAutoRenewalCertificate()).isFalse();\n            softly.assertThat(order.getCertificate().getLocation())\n                    .isEqualTo(url(\"https://example.com/acme/cert/1234\"));\n            softly.assertThat(order.getFinalizeLocation()).isEqualTo(finalizeUrl);\n\n            var auths = order.getAuthorizations();\n            softly.assertThat(auths).hasSize(2);\n            softly.assertThat(auths.stream())\n                    .map(Authorization::getLocation)\n                    .containsExactlyInAnyOrder(\n                            url(\"https://example.com/acme/authz/1234\"),\n                            url(\"https://example.com/acme/authz/2345\"));\n        }\n\n        provider.close();\n    }\n\n    /**\n     * Test that order is properly updated.\n     */\n    @Test\n    public void testAutoRenewUpdate() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"updateAutoRenewOrderResponse\");\n            }\n        };\n\n        provider.putMetadata(\"auto-renewal\", JSON.empty());\n\n        var login = provider.createLogin();\n\n        var order = new Order(login, locationUrl);\n        order.fetch();\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(order.isAutoRenewing()).isTrue();\n            softly.assertThat(order.getAutoRenewalStartDate().orElseThrow())\n                    .isEqualTo(\"2016-01-01T00:00:00Z\");\n            softly.assertThat(order.getAutoRenewalEndDate())\n                    .isEqualTo(\"2017-01-01T00:00:00Z\");\n            softly.assertThat(order.getAutoRenewalLifetime())\n                    .isEqualTo(Duration.ofHours(168));\n            softly.assertThat(order.getAutoRenewalLifetimeAdjust().orElseThrow())\n                    .isEqualTo(Duration.ofDays(6));\n            softly.assertThat(order.getNotBefore()).isEmpty();\n            softly.assertThat(order.getNotAfter()).isEmpty();\n            softly.assertThat(order.isAutoRenewalGetEnabled()).isTrue();\n        }\n\n        provider.close();\n    }\n\n    /**\n     * Test that auto-renew order is properly finalized.\n     */\n    @Test\n    public void testAutoRenewFinalize() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"finalizeAutoRenewResponse\");\n            }\n        };\n\n        var login = provider.createLogin();\n        var order = login.bindOrder(locationUrl);\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(order.isAutoRenewalCertificate()).isTrue();\n            softly.assertThat(order.getCertificate().getLocation())\n                    .isEqualTo(url(\"https://example.com/acme/cert/1234\"));\n            softly.assertThat(order.isAutoRenewing()).isTrue();\n            softly.assertThat(order.getAutoRenewalStartDate().orElseThrow())\n                    .isEqualTo(\"2018-01-01T00:00:00Z\");\n            softly.assertThat(order.getAutoRenewalEndDate())\n                    .isEqualTo(\"2019-01-01T00:00:00Z\");\n            softly.assertThat(order.getAutoRenewalLifetime())\n                    .isEqualTo(Duration.ofHours(168));\n            softly.assertThat(order.getAutoRenewalLifetimeAdjust().orElseThrow())\n                    .isEqualTo(Duration.ofDays(6));\n            softly.assertThat(order.getNotBefore()).isEmpty();\n            softly.assertThat(order.getNotAfter()).isEmpty();\n            softly.assertThat(order.isAutoRenewalGetEnabled()).isTrue();\n        }\n\n        provider.close();\n    }\n\n    /**\n     * Test that auto-renew order is properly canceled.\n     */\n    @Test\n    public void testCancel() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                var json = claims.toJSON();\n                assertThat(json.get(\"status\").asString()).isEqualTo(\"canceled\");\n                assertThat(url).isEqualTo(locationUrl);\n                assertThat(login).isNotNull();\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"canceledOrderResponse\");\n            }\n        };\n\n        provider.putMetadata(\"auto-renewal\", JSON.empty());\n\n        var login = provider.createLogin();\n\n        var order = new Order(login, locationUrl);\n        order.cancelAutoRenewal();\n\n        assertThat(order.getStatus()).isEqualTo(Status.CANCELED);\n\n        provider.close();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/ProblemTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.net.URI;\n\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link Problem}.\n */\npublic class ProblemTest {\n\n    @Test\n    public void testProblem() {\n        var baseUrl = url(\"https://example.com/acme/1\");\n        var original = TestUtils.getJSON(\"problem\");\n\n        var problem = new Problem(original, baseUrl);\n\n        assertThatJson(problem.asJSON().toString()).isEqualTo(original.toString());\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(problem.getType()).isEqualTo(URI.create(\"urn:ietf:params:acme:error:malformed\"));\n            softly.assertThat(problem.getTitle().orElseThrow())\n                    .isEqualTo(\"Some of the identifiers requested were rejected\");\n            softly.assertThat(problem.getDetail().orElseThrow())\n                    .isEqualTo(\"Identifier \\\"abc12_\\\" is malformed\");\n            softly.assertThat(problem.getInstance().orElseThrow())\n                    .isEqualTo(URI.create(\"https://example.com/documents/error.html\"));\n            softly.assertThat(problem.getIdentifier()).isEmpty();\n            softly.assertThat(problem.toString()).isEqualTo(\n                    \"Identifier \\\"abc12_\\\" is malformed (\"\n                            + \"Invalid underscore in DNS name \\\"_example.com\\\" ‒ \"\n                            + \"This CA will not issue for \\\"example.net\\\")\");\n\n            var subs = problem.getSubProblems();\n            softly.assertThat(subs).isNotNull().hasSize(2);\n\n            var p1 = subs.get(0);\n            softly.assertThat(p1.getType()).isEqualTo(URI.create(\"urn:ietf:params:acme:error:malformed\"));\n            softly.assertThat(p1.getTitle()).isEmpty();\n            softly.assertThat(p1.getDetail().orElseThrow())\n                    .isEqualTo(\"Invalid underscore in DNS name \\\"_example.com\\\"\");\n            softly.assertThat(p1.getIdentifier().orElseThrow().getDomain()).isEqualTo(\"_example.com\");\n            softly.assertThat(p1.toString()).isEqualTo(\"Invalid underscore in DNS name \\\"_example.com\\\"\");\n\n            var p2 = subs.get(1);\n            softly.assertThat(p2.getType()).isEqualTo(URI.create(\"urn:ietf:params:acme:error:rejectedIdentifier\"));\n            softly.assertThat(p2.getTitle()).isEmpty();\n            softly.assertThat(p2.getDetail().orElseThrow())\n                    .isEqualTo(\"This CA will not issue for \\\"example.net\\\"\");\n            softly.assertThat(p2.getIdentifier().orElseThrow().getDomain()).isEqualTo(\"example.net\");\n            softly.assertThat(p2.toString()).isEqualTo(\"This CA will not issue for \\\"example.net\\\"\");\n        }\n    }\n\n    /**\n     * Test that {@link Problem#toString()} always returns the most specific message.\n     */\n    @Test\n    public void testToString() {\n        var baseUrl = url(\"https://example.com/acme/1\");\n        var typeUri = URI.create(\"urn:ietf:params:acme:error:malformed\");\n\n        var jb = new JSONBuilder();\n\n        jb.put(\"type\", typeUri);\n        var p1 = new Problem(jb.toJSON(), baseUrl);\n        assertThat(p1.toString()).isEqualTo(typeUri.toString());\n\n        jb.put(\"title\", \"Some of the identifiers requested were rejected\");\n        var p2 = new Problem(jb.toJSON(), baseUrl);\n        assertThat(p2.toString()).isEqualTo(\"Some of the identifiers requested were rejected\");\n\n        jb.put(\"detail\", \"Identifier \\\"abc12_\\\" is malformed\");\n        var p3 = new Problem(jb.toJSON(), baseUrl);\n        assertThat(p3.toString()).isEqualTo(\"Identifier \\\"abc12_\\\" is malformed\");\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/RenewalInfoTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2023 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatExceptionOfType;\nimport static org.mockito.Mockito.mock;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.ZonedDateTime;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Optional;\n\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.provider.TestableConnectionProvider;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * Unit test for {@link RenewalInfo}.\n */\npublic class RenewalInfoTest {\n\n    private final URL locationUrl = url(\"http://example.com/acme/renewalInfo/1234\");\n    private final Instant retryAfterInstant = Instant.now().plus(10L, ChronoUnit.DAYS);\n    private final Instant startWindow = Instant.parse(\"2021-01-03T00:00:00Z\");\n    private final Instant endWindow = Instant.parse(\"2021-01-07T00:00:00Z\");\n\n    @Test\n    public void testGetters() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendRequest(URL url, Session session, ZonedDateTime ifModifiedSince) {\n                assertThat(url).isEqualTo(locationUrl);\n                assertThat(session).isNotNull();\n                assertThat(ifModifiedSince).isNull();\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"renewalInfo\");\n            }\n\n            @Override\n            public Optional<Instant> getRetryAfter() {\n                return Optional.of(retryAfterInstant);\n            }\n        };\n\n        var login = provider.createLogin();\n\n        var renewalInfo = new RenewalInfo(login, locationUrl);\n        var recheckAfter = renewalInfo.fetch();\n        assertThat(recheckAfter).hasValue(retryAfterInstant);\n\n        // Check getters\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(renewalInfo.getLocation()).isEqualTo(locationUrl);\n            softly.assertThat(renewalInfo.getSuggestedWindowStart())\n                    .isEqualTo(startWindow);\n            softly.assertThat(renewalInfo.getSuggestedWindowEnd())\n                    .isEqualTo(endWindow);\n            softly.assertThat(renewalInfo.getExplanation())\n                    .isNotEmpty()\n                    .contains(url(\"https://example.com/docs/example-mass-reissuance-event\"));\n        }\n\n        // Check renewalIsNotRequired\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(renewalInfo.renewalIsNotRequired(startWindow.minusSeconds(1L)))\n                    .isTrue();\n            softly.assertThat(renewalInfo.renewalIsNotRequired(startWindow))\n                    .isFalse();\n            softly.assertThat(renewalInfo.renewalIsNotRequired(endWindow.minusSeconds(1L)))\n                    .isFalse();\n            softly.assertThat(renewalInfo.renewalIsNotRequired(endWindow))\n                    .isFalse();\n        }\n\n        // Check renewalIsRecommended\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(renewalInfo.renewalIsRecommended(startWindow.minusSeconds(1L)))\n                    .isFalse();\n            softly.assertThat(renewalInfo.renewalIsRecommended(startWindow))\n                    .isTrue();\n            softly.assertThat(renewalInfo.renewalIsRecommended(endWindow.minusSeconds(1L)))\n                    .isTrue();\n            softly.assertThat(renewalInfo.renewalIsRecommended(endWindow))\n                    .isFalse();\n        }\n\n        // Check renewalIsOverdue\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(renewalInfo.renewalIsOverdue(startWindow.minusSeconds(1L)))\n                    .isFalse();\n            softly.assertThat(renewalInfo.renewalIsOverdue(startWindow))\n                    .isFalse();\n            softly.assertThat(renewalInfo.renewalIsOverdue(endWindow.minusSeconds(1L)))\n                    .isFalse();\n            softly.assertThat(renewalInfo.renewalIsOverdue(endWindow))\n                    .isTrue();\n        }\n\n        // Check getRandomProposal, is empty because end window is in the past\n        var proposal = renewalInfo.getRandomProposal(null);\n        assertThat(proposal).isEmpty();\n\n        provider.close();\n    }\n\n    @Test\n    public void testRandomProposal() {\n        var login = mock(Login.class);\n        var start = Instant.now();\n        var end = start.plus(1L, ChronoUnit.DAYS);\n\n        var renewalInfo = new RenewalInfo(login, locationUrl) {\n            @Override\n            public Instant getSuggestedWindowStart() {\n                return start;\n            }\n\n            @Override\n            public Instant getSuggestedWindowEnd() {\n                return end;\n            }\n        };\n\n        var noFreq = renewalInfo.getRandomProposal(null);\n        assertThat(noFreq).isNotEmpty();\n        assertThat(noFreq.get()).isBetween(start, end);\n\n        var oneHour = renewalInfo.getRandomProposal(Duration.ofHours(1L));\n        assertThat(oneHour).isNotEmpty();\n        assertThat(oneHour.get()).isBetween(start, end.minus(1L, ChronoUnit.HOURS));\n\n        var twoDays = renewalInfo.getRandomProposal(Duration.ofDays(2L));\n        assertThat(twoDays).isEmpty();\n    }\n\n    @Test\n    public void testDateAssertion() {\n        var login = mock(Login.class);\n        var start = Instant.now();\n        var end = start.minusSeconds(1L);  // end before start\n\n        var renewalInfo = new RenewalInfo(login, locationUrl) {\n            @Override\n            public Instant getSuggestedWindowStart() {\n                return start;\n            }\n\n            @Override\n            public Instant getSuggestedWindowEnd() {\n                return end;\n            }\n        };\n\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(() -> renewalInfo.renewalIsRecommended(start));\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatExceptionOfType;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.Mockito.*;\nimport static org.shredzone.acme4j.toolbox.TestUtils.*;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.time.Duration;\nimport java.time.ZonedDateTime;\nimport java.util.Locale;\n\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentMatchers;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeNotSupportedException;\nimport org.shredzone.acme4j.provider.AcmeProvider;\nimport org.shredzone.acme4j.provider.GenericAcmeProvider;\nimport org.shredzone.acme4j.toolbox.AcmeUtils;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit test for {@link Session}.\n */\npublic class SessionTest {\n\n    /**\n     * Test constructor\n     */\n    @Test\n    public void testConstructor() {\n        var serverUri = URI.create(TestUtils.ACME_SERVER_URI);\n\n        assertThrows(NullPointerException.class, () -> new Session((URI) null));\n\n        var session = new Session(serverUri);\n        assertThat(session).isNotNull();\n        assertThat(session.getServerUri()).isEqualTo(serverUri);\n\n        var session2 = new Session(TestUtils.ACME_SERVER_URI);\n        assertThat(session2).isNotNull();\n        assertThat(session2.getServerUri()).isEqualTo(serverUri);\n\n        var session3 = new Session(serverUri, new GenericAcmeProvider());\n        assertThat(session3).isNotNull();\n        assertThat(session3.getServerUri()).isEqualTo(serverUri);\n\n        assertThrows(IllegalArgumentException.class,\n                () -> new Session(\"#*aBaDuRi*#\"),\n                \"Bad URI in constructor\");\n        assertThrows(IllegalArgumentException.class,\n                () -> new Session(URI.create(\"acme://invalid\"), new GenericAcmeProvider()),\n                \"Unsupported URI\");\n    }\n\n    /**\n     * Test getters and setters.\n     */\n    @Test\n    public void testGettersAndSetters() {\n        var serverUri = URI.create(TestUtils.ACME_SERVER_URI);\n        var now = ZonedDateTime.now();\n\n        var session = new Session(serverUri);\n\n        try (var nonceHolder = session.lockNonce()) {\n            assertThat(nonceHolder.getNonce()).isNull();\n            nonceHolder.setNonce(DUMMY_NONCE);\n            assertThat(nonceHolder.getNonce()).isEqualTo(DUMMY_NONCE);\n        }\n\n        assertThat(session.getServerUri()).isEqualTo(serverUri);\n        assertThat(session.networkSettings()).isNotNull();\n\n        assertThat(session.getDirectoryExpires()).isNull();\n        session.setDirectoryExpires(now);\n        assertThat(session.getDirectoryExpires()).isEqualTo(now);\n        session.setDirectoryExpires(null);\n        assertThat(session.getDirectoryExpires()).isNull();\n\n        assertThat(session.getDirectoryLastModified()).isNull();\n        session.setDirectoryLastModified(now);\n        assertThat(session.getDirectoryLastModified()).isEqualTo(now);\n        session.setDirectoryLastModified(null);\n        assertThat(session.getDirectoryLastModified()).isNull();\n\n        session.setDirectoryExpires(now);\n        session.setDirectoryLastModified(now);\n        session.purgeDirectoryCache();\n        assertThat(session.getDirectoryExpires()).isNull();\n        assertThat(session.getDirectoryLastModified()).isNull();\n        assertThat(session.hasDirectory()).isFalse();\n    }\n\n    /**\n     * Test login methods.\n     */\n    @Test\n    public void testLogin() throws IOException {\n        var serverUri = URI.create(TestUtils.ACME_SERVER_URI);\n        var accountLocation = url(TestUtils.ACCOUNT_URL);\n        var accountKeyPair = TestUtils.createKeyPair();\n\n        var session = new Session(serverUri);\n\n        var login = session.login(accountLocation, accountKeyPair);\n        assertThat(login).isNotNull();\n        assertThat(login.getSession()).isEqualTo(session);\n        assertThat(login.getAccount().getLocation()).isEqualTo(accountLocation);\n        assertThat(login.getPublicKey()).isEqualTo(accountKeyPair.getPublic());\n    }\n\n    /**\n     * Test that the directory is properly read.\n     */\n    @Test\n    public void testDirectory() throws AcmeException, IOException {\n        var serverUri = URI.create(TestUtils.ACME_SERVER_URI);\n\n        var mockProvider = mock(AcmeProvider.class);\n        when(mockProvider.directory(\n                        ArgumentMatchers.any(Session.class),\n                        ArgumentMatchers.eq(serverUri)))\n                .thenReturn(getJSON(\"directory\"));\n\n        var session = new Session(serverUri) {\n            @Override\n            public AcmeProvider provider() {\n                return mockProvider;\n            }\n        };\n\n        // No directory has been fetched yet\n        assertThat(session.hasDirectory()).isFalse();\n\n        assertThat(session.resourceUrl(Resource.NEW_ACCOUNT))\n                .isEqualTo(URI.create(\"https://example.com/acme/new-account\").toURL());\n\n        // There is a local copy of the directory now\n        assertThat(session.hasDirectory()).isTrue();\n\n        assertThat(session.resourceUrl(Resource.NEW_AUTHZ))\n                .isEqualTo(URI.create(\"https://example.com/acme/new-authz\").toURL());\n        assertThat(session.resourceUrl(Resource.NEW_ORDER))\n                .isEqualTo(URI.create(\"https://example.com/acme/new-order\").toURL());\n\n        assertThatExceptionOfType(AcmeNotSupportedException.class)\n                .isThrownBy(() -> session.resourceUrl(Resource.REVOKE_CERT))\n                .withMessage(\"Server does not support revokeCert\");\n\n        assertThat(session.resourceUrlOptional(Resource.NEW_AUTHZ))\n                .isNotEmpty()\n                .contains(URI.create(\"https://example.com/acme/new-authz\").toURL());\n\n        assertThat(session.resourceUrlOptional(Resource.REVOKE_CERT))\n                .isEmpty();\n\n        var meta = session.getMetadata();\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(meta).isNotNull();\n            softly.assertThat(meta.getTermsOfService().orElseThrow())\n                    .isEqualTo(URI.create(\"https://example.com/acme/terms\"));\n            softly.assertThat(meta.getWebsite().orElseThrow().toExternalForm())\n                    .isEqualTo(\"https://www.example.com/\");\n            softly.assertThat(meta.getCaaIdentities()).containsExactlyInAnyOrder(\"example.com\");\n            softly.assertThat(meta.isAutoRenewalEnabled()).isTrue();\n            softly.assertThat(meta.getAutoRenewalMaxDuration()).isEqualTo(Duration.ofDays(365));\n            softly.assertThat(meta.getAutoRenewalMinLifetime()).isEqualTo(Duration.ofHours(24));\n            softly.assertThat(meta.isAutoRenewalGetAllowed()).isTrue();\n            softly.assertThat(meta.isProfileAllowed()).isTrue();\n            softly.assertThat(meta.isProfileAllowed(\"classic\")).isTrue();\n            softly.assertThat(meta.isProfileAllowed(\"custom\")).isTrue();\n            softly.assertThat(meta.isProfileAllowed(\"invalid\")).isFalse();\n            softly.assertThat(meta.getProfileDescription(\"classic\")).contains(\"The profile you're accustomed to\");\n            softly.assertThat(meta.getProfileDescription(\"custom\")).contains(\"Some other profile\");\n            softly.assertThat(meta.getProfiles()).contains(\"classic\", \"custom\");\n            softly.assertThat(meta.getProfileDescription(\"invalid\")).isEmpty();\n            softly.assertThat(meta.isExternalAccountRequired()).isTrue();\n            softly.assertThat(meta.isSubdomainAuthAllowed()).isTrue();\n            softly.assertThat(meta.getJSON()).isNotNull();\n        }\n\n        // Make sure directory is read\n        verify(mockProvider, atLeastOnce()).directory(\n                        ArgumentMatchers.any(Session.class),\n                        ArgumentMatchers.any(URI.class));\n    }\n\n    /**\n     * Test that the directory is properly read even if there are no metadata.\n     */\n    @Test\n    public void testNoMeta() throws AcmeException, IOException {\n        var serverUri = URI.create(TestUtils.ACME_SERVER_URI);\n\n        var mockProvider = mock(AcmeProvider.class);\n        when(mockProvider.directory(\n                        ArgumentMatchers.any(Session.class),\n                        ArgumentMatchers.eq(serverUri)))\n                .thenReturn(getJSON(\"directoryNoMeta\"));\n\n        var session = new Session(serverUri) {\n            @Override\n            public AcmeProvider provider() {\n                return mockProvider;\n            }\n        };\n\n        assertThat(session.resourceUrl(Resource.NEW_ACCOUNT))\n                .isEqualTo(URI.create(\"https://example.com/acme/new-account\").toURL());\n        assertThat(session.resourceUrl(Resource.NEW_AUTHZ))\n                .isEqualTo(URI.create(\"https://example.com/acme/new-authz\").toURL());\n        assertThat(session.resourceUrl(Resource.NEW_ORDER))\n                .isEqualTo(URI.create(\"https://example.com/acme/new-order\").toURL());\n\n        var meta = session.getMetadata();\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(meta).isNotNull();\n            softly.assertThat(meta.getTermsOfService()).isEmpty();\n            softly.assertThat(meta.getWebsite()).isEmpty();\n            softly.assertThat(meta.getCaaIdentities()).isEmpty();\n            softly.assertThat(meta.isAutoRenewalEnabled()).isFalse();\n            softly.assertThat(meta.isSubdomainAuthAllowed()).isFalse();\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(meta::getAutoRenewalMaxDuration);\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(meta::getAutoRenewalMinLifetime);\n            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)\n                    .isThrownBy(meta::isAutoRenewalGetAllowed);\n            softly.assertThat(meta.isProfileAllowed()).isFalse();\n            softly.assertThat(meta.isProfileAllowed(\"classic\")).isFalse();\n            softly.assertThat(meta.getProfileDescription(\"classic\")).isEmpty();\n            softly.assertThat(meta.getProfiles()).isEmpty();\n        }\n    }\n\n    /**\n     * Test that the locale is properly set.\n     */\n    @Test\n    public void testLocale() {\n        var session = new Session(URI.create(TestUtils.ACME_SERVER_URI));\n\n        // default configuration\n        assertThat(session.getLocale())\n                .isEqualTo(Locale.getDefault());\n        assertThat(session.getLanguageHeader())\n                .isEqualTo(AcmeUtils.localeToLanguageHeader(Locale.getDefault()));\n\n        // null\n        session.setLocale(null);\n        assertThat(session.getLocale()).isNull();\n        assertThat(session.getLanguageHeader()).isEqualTo(\"*\");\n\n        // a locale\n        session.setLocale(Locale.CANADA_FRENCH);\n        assertThat(session.getLocale()).isEqualTo(Locale.CANADA_FRENCH);\n        assertThat(session.getLanguageHeader()).isEqualTo(\"fr-CA,fr;q=0.8,*;q=0.1\");\n    }\n\n    /**\n     * Test that getHttpClient returns a shared client instance.\n     */\n    @Test\n    public void testGetHttpClientWithReuse() {\n        var session = new Session(URI.create(TestUtils.ACME_SERVER_URI));\n\n        var client1 = session.getHttpClient();\n        var client2 = session.getHttpClient();\n\n        // Both calls should return the same client instance\n        assertThat(client1).isSameAs(client2);\n    }\n\n    /**\n     * Test that getHttpClient is thread-safe.\n     */\n    @Test\n    public void testGetHttpClientThreadSafety() throws Exception {\n        var session = new Session(URI.create(TestUtils.ACME_SERVER_URI));\n\n        var threads = new Thread[10];\n        var clients = new java.net.http.HttpClient[threads.length];\n\n        for (int i = 0; i < threads.length; i++) {\n            final int index = i;\n            threads[i] = new Thread(() -> {\n                clients[index] = session.getHttpClient();\n            });\n        }\n\n        for (var thread : threads) {\n            thread.start();\n        }\n\n        for (var thread : threads) {\n            thread.join();\n        }\n\n        // All threads should get the same client instance\n        var firstClient = clients[0];\n        for (var client : clients) {\n            assertThat(client).isSameAs(firstClient);\n        }\n    }\n\n    /**\n     * Test that connections from the same session share the same HttpClient.\n     */\n    @Test\n    public void testConnectionsShareHttpClient() throws AcmeException {\n        var session = new Session(URI.create(TestUtils.ACME_SERVER_URI));\n\n        var conn1 = session.connect();\n        var conn2 = session.connect();\n\n        // Both connections should use the same HttpClient from the session\n        var client1 = session.getHttpClient();\n        var client2 = session.getHttpClient();\n        assertThat(client1).isSameAs(client2);\n\n        conn1.close();\n        conn2.close();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/StatusTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.Locale;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link Status} enumeration.\n */\npublic class StatusTest {\n\n    /**\n     * Test that invoking {@link Status#parse(String)} gives the correct status.\n     */\n    @Test\n    public void testParse() {\n        // Would break toUpperCase() if English locale is not set, see #156.\n        Locale.setDefault(new Locale(\"tr\"));\n\n        for (var s : Status.values()) {\n            var parsed = Status.parse(s.name().toLowerCase(Locale.ENGLISH));\n            assertThat(parsed).isEqualTo(s);\n        }\n\n        // unknown status returns UNKNOWN\n        assertThat(Status.parse(\"foo\")).isEqualTo(Status.UNKNOWN);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/challenge/ChallengeTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.within;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.net.HttpURLConnection;\nimport java.net.URI;\nimport java.net.URL;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Optional;\n\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.provider.TestableConnectionProvider;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link Challenge}.\n */\npublic class ChallengeTest {\n    private final URL locationUrl = url(\"https://example.com/acme/some-location\");\n\n    /**\n     * Test that after unmarshaling, the challenge properties are set correctly.\n     */\n    @Test\n    public void testUnmarshal() {\n        var challenge = new Challenge(TestUtils.login(), getJSON(\"genericChallenge\"));\n\n        // Test unmarshalled values\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(challenge.getType()).isEqualTo(\"generic-01\");\n            softly.assertThat(challenge.getStatus()).isEqualTo(Status.INVALID);\n            softly.assertThat(challenge.getLocation()).isEqualTo(url(\"http://example.com/challenge/123\"));\n            softly.assertThat(challenge.getValidated().orElseThrow())\n                    .isCloseTo(\"2015-12-12T17:19:36.336Z\", within(1, ChronoUnit.MILLIS));\n            softly.assertThat(challenge.getJSON().get(\"type\").asString()).isEqualTo(\"generic-01\");\n            softly.assertThat(challenge.getJSON().get(\"url\").asURL()).isEqualTo(url(\"http://example.com/challenge/123\"));\n\n            var error = challenge.getError().orElseThrow();\n            softly.assertThat(error.getType()).isEqualTo(URI.create(\"urn:ietf:params:acme:error:incorrectResponse\"));\n            softly.assertThat(error.getDetail().orElseThrow()).isEqualTo(\"bad token\");\n            softly.assertThat(error.getInstance().orElseThrow())\n                    .isEqualTo(URI.create(\"http://example.com/documents/faq.html\"));\n        }\n    }\n\n    /**\n     * Test that {@link Challenge#prepareResponse(JSONBuilder)} contains the type.\n     */\n    @Test\n    public void testRespond() {\n        var challenge = new Challenge(TestUtils.login(), getJSON(\"genericChallenge\"));\n\n        var response = new JSONBuilder();\n        challenge.prepareResponse(response);\n\n        assertThatJson(response.toString()).isEqualTo(\"{}\");\n    }\n\n    /**\n     * Test that an exception is thrown on challenge type mismatch.\n     */\n    @Test\n    public void testNotAcceptable() {\n        assertThrows(AcmeProtocolException.class, () ->\n            new Http01Challenge(TestUtils.login(), getJSON(\"dns01Challenge\"))\n        );\n    }\n\n    /**\n     * Test that a challenge can be triggered.\n     */\n    @Test\n    public void testTrigger() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                assertThatJson(claims.toString()).isEqualTo(getJSON(\"triggerHttpChallengeRequest\").toString());\n                assertThat(login).isNotNull();\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"triggerHttpChallengeResponse\");\n            }\n        };\n\n        var login = provider.createLogin();\n\n        var challenge = new Http01Challenge(login, getJSON(\"triggerHttpChallenge\"));\n\n        challenge.trigger();\n\n        assertThat(challenge.getStatus()).isEqualTo(Status.PENDING);\n        assertThat(challenge.getLocation()).isEqualTo(locationUrl);\n\n        provider.close();\n    }\n\n    /**\n     * Test that a challenge is properly updated.\n     */\n    @Test\n    public void testUpdate() throws Exception {\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"updateHttpChallengeResponse\");\n            }\n        };\n\n        var login = provider.createLogin();\n\n        var challenge = new Http01Challenge(login, getJSON(\"triggerHttpChallengeResponse\"));\n\n        challenge.fetch();\n\n        assertThat(challenge.getStatus()).isEqualTo(Status.VALID);\n        assertThat(challenge.getLocation()).isEqualTo(locationUrl);\n\n        provider.close();\n    }\n\n    /**\n     * Test that a challenge is properly updated, with Retry-After header.\n     */\n    @Test\n    public void testUpdateRetryAfter() throws Exception {\n        var retryAfter = Instant.now().plus(Duration.ofSeconds(30));\n\n        var provider = new TestableConnectionProvider() {\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                assertThat(url).isEqualTo(locationUrl);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                return getJSON(\"updateHttpChallengeResponse\");\n            }\n\n            @Override\n            public Optional<Instant> getRetryAfter() {\n                return Optional.of(retryAfter);\n            }\n        };\n\n        var login = provider.createLogin();\n\n        var challenge = new Http01Challenge(login, getJSON(\"triggerHttpChallengeResponse\"));\n        var returnedRetryAfter = challenge.fetch();\n        assertThat(returnedRetryAfter).hasValue(retryAfter);\n\n        assertThat(challenge.getStatus()).isEqualTo(Status.VALID);\n        assertThat(challenge.getLocation()).isEqualTo(locationUrl);\n\n        provider.close();\n    }\n\n    /**\n     * Test that unmarshalling something different like a challenge fails.\n     */\n    @Test\n    public void testBadUnmarshall() {\n        assertThrows(AcmeProtocolException.class, () ->\n            new Challenge(TestUtils.login(), getJSON(\"updateAccountResponse\"))\n        );\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/challenge/Dns01ChallengeTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatExceptionOfType;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link Dns01Challenge}.\n */\npublic class Dns01ChallengeTest {\n\n    private final Login login = TestUtils.login();\n\n    /**\n     * Test that {@link Dns01Challenge} generates a correct authorization key.\n     */\n    @Test\n    public void testDnsChallenge() {\n        var challenge = new Dns01Challenge(login, getJSON(\"dns01Challenge\"));\n\n        assertThat(challenge.getType()).isEqualTo(Dns01Challenge.TYPE);\n        assertThat(challenge.getStatus()).isEqualTo(Status.PENDING);\n        assertThat(challenge.getDigest()).isEqualTo(\"rzMmotrIgsithyBYc0vgiLUEEKYx0WetQRgEF2JIozA\");\n        assertThat(challenge.getAuthorization()).isEqualTo(\"pNvmJivs0WCko2suV7fhe-59oFqyYx_yB7tx6kIMAyE.HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0\");\n\n        assertThat(challenge.getRRName(\"www.example.org\")).isEqualTo(\"_acme-challenge.www.example.org.\");\n        assertThat(challenge.getRRName(Identifier.dns(\"www.example.org\"))).isEqualTo(\"_acme-challenge.www.example.org.\");\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(() -> challenge.getRRName(Identifier.ip(\"127.0.0.10\")));\n\n        var response = new JSONBuilder();\n        challenge.prepareResponse(response);\n\n        assertThatJson(response.toString()).isEqualTo(\"{}\");\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/challenge/DnsAccount01ChallengeTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2025 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatExceptionOfType;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link DnsAccount01Challenge}.\n */\nclass DnsAccount01ChallengeTest {\n\n    private final Login login = TestUtils.login();\n\n    /**\n     * Test that {@link DnsAccount01Challenge} generates a correct authorization key.\n     */\n    @Test\n    public void testDnsChallenge() {\n        var challenge = new DnsAccount01Challenge(login, getJSON(\"dnsAccount01Challenge\"));\n\n        assertThat(challenge.getType()).isEqualTo(DnsAccount01Challenge.TYPE);\n        assertThat(challenge.getStatus()).isEqualTo(Status.PENDING);\n        assertThat(challenge.getDigest()).isEqualTo(\"MSB8ZUQOmbNfHors7PG580PBz4f9hDuOPDN_j1bNcXI\");\n        assertThat(challenge.getAuthorization()).isEqualTo(\"ODE4OWY4NTktYjhmYS00YmY1LTk5MDgtZTFjYTZmNjZlYTUx.HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0\");\n\n        assertThat(challenge.getRRName(\"www.example.org\"))\n                .isEqualTo(\"_agozs7u2dml4wbyd._acme-challenge.www.example.org.\");\n        assertThat(challenge.getRRName(Identifier.dns(\"www.example.org\")))\n                .isEqualTo(\"_agozs7u2dml4wbyd._acme-challenge.www.example.org.\");\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(() -> challenge.getRRName(Identifier.ip(\"127.0.0.10\")));\n\n        var response = new JSONBuilder();\n        challenge.prepareResponse(response);\n\n        assertThatJson(response.toString()).isEqualTo(\"{}\");\n    }\n\n}"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/challenge/DnsPersist01ChallengeTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2026 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.*;\nimport static org.shredzone.acme4j.toolbox.TestUtils.ACCOUNT_URL;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\n\nimport java.time.Instant;\nimport java.util.TreeMap;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link DnsPersist01Challenge}.\n */\nclass DnsPersist01ChallengeTest {\n\n    private final Login login = TestUtils.login();\n\n    /**\n     * Test that {@link DnsPersist01Challenge} generates a correct TXT record.\n     */\n    @Test\n    public void testDnsChallenge() {\n        var challenge = new DnsPersist01Challenge(login, getJSON(\"dnsPersist01Challenge\"));\n\n        assertThat(challenge.getType()).isEqualTo(DnsPersist01Challenge.TYPE);\n        assertThat(challenge.getStatus()).isEqualTo(Status.PENDING);\n        assertThat(challenge.getIssuerDomainNames()).containsExactly(\"authority.example\", \"ca.example.net\");\n\n        assertThat(challenge.getRRName(\"www.example.org\"))\n                .isEqualTo(\"_validation-persist.www.example.org.\");\n        assertThat(challenge.getRRName(Identifier.dns(\"www.example.org\")))\n                .isEqualTo(\"_validation-persist.www.example.org.\");\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(() -> challenge.getRRName(Identifier.ip(\"127.0.0.10\")));\n\n        assertThat(challenge.getRData())\n                .isEqualTo(\"\\\"authority.example;\\\" \\\" accounturi=\" + ACCOUNT_URL + \"\\\"\");\n\n        assertThat(challenge.getAccountUrl().toString())\n                .isEqualTo(ACCOUNT_URL);\n\n        var response = new JSONBuilder();\n        challenge.prepareResponse(response);\n\n        assertThatJson(response.toString()).isEqualTo(\"{}\");\n    }\n\n    /**\n     * Test that {@link DnsPersist01Challenge} generates a correct TXT record.\n     */\n    @Test\n    public void testBuilder() {\n        var challenge = new DnsPersist01Challenge(login, getJSON(\"dnsPersist01Challenge\"));\n        var until = Instant.ofEpochSecond(1767225600L);\n\n        assertThat(challenge.buildRData().build())\n                .isEqualTo(\"\\\"authority.example;\\\" \\\" accounturi=\" + ACCOUNT_URL + \"\\\"\");\n\n        assertThat(challenge.buildRData().wildcard().build())\n                .isEqualTo(\"\\\"authority.example;\\\" \\\" accounturi=\" + ACCOUNT_URL + \";\\\" \\\" policy=wildcard\\\"\");\n\n        assertThat(challenge.buildRData().issuerDomainName(\"ca.example.net\").build())\n                .isEqualTo(\"\\\"ca.example.net;\\\" \\\" accounturi=\" + ACCOUNT_URL + \"\\\"\");\n\n        assertThat(challenge.buildRData().persistUntil(until).build())\n                .isEqualTo(\"\\\"authority.example;\\\" \\\" accounturi=\" + ACCOUNT_URL + \";\\\" \\\" persistUntil=1767225600\\\"\");\n\n        assertThat(challenge.buildRData()\n                .wildcard()\n                .issuerDomainName(\"ca.example.net\")\n                .persistUntil(until)\n                .build()\n        ).isEqualTo(\"\\\"ca.example.net;\\\" \\\" accounturi=\" + ACCOUNT_URL + \";\\\" \\\" policy=wildcard;\\\" \\\" persistUntil=1767225600\\\"\");\n\n        assertThatIllegalArgumentException()\n                .isThrownBy(() -> challenge.buildRData().issuerDomainName(\"ca.invalid\").build())\n                .withMessage(\"Domain ca.invalid is not in the list of issuer-domain-names\");\n    }\n\n    /**\n     * Test that {@link DnsPersist01Challenge} generates a correct TXT record, without\n     * quotes.\n     */\n    @Test\n    public void testBuilderNoQuotes() {\n        var challenge = new DnsPersist01Challenge(login, getJSON(\"dnsPersist01Challenge\"));\n        var until = Instant.ofEpochSecond(1767225600L);\n\n        assertThat(challenge.buildRData().noQuotes().build())\n                .isEqualTo(\"authority.example; accounturi=\" + ACCOUNT_URL);\n\n        assertThat(challenge.buildRData()\n                .wildcard()\n                .issuerDomainName(\"ca.example.net\")\n                .persistUntil(until)\n                .noQuotes()\n                .build()\n        ).isEqualTo(\"ca.example.net; accounturi=\" + ACCOUNT_URL + \"; policy=wildcard; persistUntil=1767225600\");\n    }\n\n    @Test\n    public void testConstraintChecks() {\n        var json = getJSON(\"dnsPersist01Challenge\").toMap();\n\n        // Must fail if issuer-domain-names is missing\n        var json1 = new TreeMap<>(json);\n        json1.remove(\"issuer-domain-names\");\n        var challenge1 = new DnsPersist01Challenge(login, JSON.fromMap(json1));\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(challenge1::getIssuerDomainNames)\n                .withMessage(\"issuer-domain-names missing or empty\");\n\n        // Must fail if issuer-domain-names is empty\n        var json2 = new TreeMap<>(json);\n        json2.put(\"issuer-domain-names\", new String[0]);\n        var challenge2 = new DnsPersist01Challenge(login, JSON.fromMap(json2));\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(challenge2::getIssuerDomainNames)\n                .withMessage(\"issuer-domain-names missing or empty\");\n\n        // Must not fail if issuer-domain-names contains exactly 10 records\n        var json3 = new TreeMap<>(json);\n        json3.put(\"issuer-domain-names\", createDomainList(10));\n        var challenge3 = new DnsPersist01Challenge(login, JSON.fromMap(json3));\n        assertThatNoException()\n                .isThrownBy(challenge3::getIssuerDomainNames);\n\n        // Must fail if issuer-domain-names contains more than 10 records\n        var json4 = new TreeMap<>(json);\n        json4.put(\"issuer-domain-names\", createDomainList(11));\n        var challenge4 = new DnsPersist01Challenge(login, JSON.fromMap(json4));\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(challenge4::getIssuerDomainNames)\n                .withMessage(\"issuer-domain-names size limit exceeded: 11 > 10\");\n\n        // Must fail if issuer-domain-names contains a trailing dot\n        var json5 = new TreeMap<>(json);\n        json5.put(\"issuer-domain-names\", new String[] {\"foo.example.com.\"});\n        var challenge5 = new DnsPersist01Challenge(login, JSON.fromMap(json5));\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(challenge5::getIssuerDomainNames)\n                .withMessage(\"issuer-domain-names must not have trailing dots\");\n\n        // Must fail if accounturi is wrong\n        var json6 = new TreeMap<>(json);\n        json6.put(\"accounturi\", \"https://wrong.example.com/bad/account/1234\");\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(() -> new DnsPersist01Challenge(login, JSON.fromMap(json6)))\n                .withMessage(\"challenge is intended for a different account: https://wrong.example.com/bad/account/1234\");\n\n        // Must not fail at the moment if accounturi is missing\n        // (downward compatiblilty for draft-ietf-acme-dns-persist-00)\n        var json7 = new TreeMap<>(json);\n        json7.remove(\"accounturi\");\n        assertThatNoException()\n                .isThrownBy(() -> new DnsPersist01Challenge(login, JSON.fromMap(json7)));\n    }\n\n    private String[] createDomainList(int length) {\n        var result = new String[length];\n        for (var ix = 0; ix < length; ix++) {\n            result[ix] = \"foo\" + ix + \".example.com\";\n        }\n        return result;\n    }\n\n}"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/challenge/Http01ChallengeTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link Http01Challenge}.\n */\npublic class Http01ChallengeTest {\n    private static final String TOKEN =\n            \"rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ\";\n    private static final String KEY_AUTHORIZATION =\n            \"rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ.HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0\";\n\n    private final Login login = TestUtils.login();\n\n    /**\n     * Test that {@link Http01Challenge} generates a correct authorization key.\n     */\n    @Test\n    public void testHttpChallenge() {\n        var challenge = new Http01Challenge(login, getJSON(\"httpChallenge\"));\n\n        assertThat(challenge.getType()).isEqualTo(Http01Challenge.TYPE);\n        assertThat(challenge.getStatus()).isEqualTo(Status.PENDING);\n        assertThat(challenge.getToken()).isEqualTo(TOKEN);\n        assertThat(challenge.getAuthorization()).isEqualTo(KEY_AUTHORIZATION);\n\n        var response = new JSONBuilder();\n        challenge.prepareResponse(response);\n\n        assertThatJson(response.toString()).isEqualTo(\"{}\");\n    }\n\n    /**\n     * Test that an exception is thrown if there is no token.\n     */\n    @Test\n    public void testNoTokenSet() {\n        assertThrows(AcmeProtocolException.class, () -> {\n            Http01Challenge challenge = new Http01Challenge(login, getJSON(\"httpNoTokenChallenge\"));\n            challenge.getToken();\n        });\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TlsAlpn01ChallengeTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatNoException;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\n\nimport java.security.cert.CertificateParsingException;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.toolbox.AcmeUtils;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\nimport org.shredzone.acme4j.util.KeyPairUtils;\n\n/**\n * Unit tests for {@link TlsAlpn01ChallengeTest}.\n */\npublic class TlsAlpn01ChallengeTest {\n    private static final String TOKEN =\n            \"rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ\";\n    private static final String KEY_AUTHORIZATION =\n            \"rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ.HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0\";\n\n    private final Login login = TestUtils.login();\n\n    /**\n     * Test that {@link TlsAlpn01Challenge} generates a correct authorization key.\n     */\n    @Test\n    public void testTlsAlpn01Challenge() {\n        var challenge = new TlsAlpn01Challenge(login, getJSON(\"tlsAlpnChallenge\"));\n\n        assertThat(challenge.getType()).isEqualTo(TlsAlpn01Challenge.TYPE);\n        assertThat(challenge.getStatus()).isEqualTo(Status.PENDING);\n        assertThat(challenge.getToken()).isEqualTo(TOKEN);\n        assertThat(challenge.getAuthorization()).isEqualTo(KEY_AUTHORIZATION);\n        assertThat(challenge.getAcmeValidation()).isEqualTo(AcmeUtils.sha256hash(KEY_AUTHORIZATION));\n\n        var response = new JSONBuilder();\n        challenge.prepareResponse(response);\n\n        assertThatJson(response.toString()).isEqualTo(\"{}\");\n    }\n\n    /**\n     * Test that {@link TlsAlpn01Challenge} generates a correct test certificate\n     */\n    @Test\n    public void testTlsAlpn01Certificate() throws CertificateParsingException {\n        var challenge = new TlsAlpn01Challenge(login, getJSON(\"tlsAlpnChallenge\"));\n        var keypair = KeyPairUtils.createKeyPair(2048);\n        var subject = Identifier.dns(\"example.com\");\n\n        var certificate = challenge.createCertificate(keypair, subject);\n\n        // Only check the main requirements. Cert generation is fully tested in CertificateUtilsTest.\n        assertThat(certificate).isNotNull();\n        assertThat(certificate.getSubjectX500Principal().getName()).isEqualTo(\"CN=acme.invalid\");\n        assertThat(certificate.getSubjectAlternativeNames().stream()\n                .map(l -> l.get(1))\n                .map(Object::toString)).contains(subject.getDomain());\n        assertThat(certificate.getCriticalExtensionOIDs()).contains(TlsAlpn01Challenge.ACME_VALIDATION_OID);\n        assertThatNoException().isThrownBy(() -> certificate.verify(keypair.getPublic()));\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TokenChallengeTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2019 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.challenge;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.io.IOException;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.provider.TestableConnectionProvider;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\n\n/**\n * Unit tests for {@link TokenChallenge}.\n */\npublic class TokenChallengeTest {\n\n    /**\n     * Test that invalid tokens are detected.\n     */\n    @Test\n    public void testInvalidToken() throws IOException {\n        var provider = new TestableConnectionProvider();\n        var login = provider.createLogin();\n\n        var jb = new JSONBuilder();\n        jb.put(\"url\", \"https://example.com/acme/1234\");\n        jb.put(\"type\", \"generic\");\n        jb.put(\"token\", \"<script>someMaliciousCode()</script>\");\n\n        var challenge = new TokenChallenge(login, jb.toJSON());\n        assertThrows(AcmeProtocolException.class, challenge::getToken);\n        provider.close();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static java.time.temporal.ChronoUnit.SECONDS;\nimport static com.github.tomakehurst.wiremock.client.WireMock.*;\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatExceptionOfType;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getResourceAsByteArray;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.OutputStreamWriter;\nimport java.net.HttpURLConnection;\nimport java.net.URI;\nimport java.net.URL;\nimport java.security.KeyPair;\nimport java.security.cert.X509Certificate;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.ZoneOffset;\nimport java.time.ZonedDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Arrays;\nimport java.util.Base64;\nimport java.util.List;\nimport java.util.Locale;\n\nimport com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;\nimport com.github.tomakehurst.wiremock.junit5.WireMockTest;\nimport org.jose4j.jws.JsonWebSignature;\nimport org.jose4j.jwx.CompactSerializer;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.exception.AcmeRateLimitedException;\nimport org.shredzone.acme4j.exception.AcmeServerException;\nimport org.shredzone.acme4j.exception.AcmeUnauthorizedException;\nimport org.shredzone.acme4j.exception.AcmeUserActionRequiredException;\nimport org.shredzone.acme4j.toolbox.AcmeUtils;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.JoseUtils;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link DefaultConnection}.\n */\n@WireMockTest\npublic class DefaultConnectionTest {\n\n    private static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder().withoutPadding();\n    private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder();\n    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC);\n    private static final String DIRECTORY_PATH = \"/dir\";\n    private static final String NEW_NONCE_PATH = \"/newNonce\";\n    private static final String REQUEST_PATH = \"/test/test\";\n    private static final String TEST_ACCEPT_LANGUAGE = \"ja-JP,ja;q=0.8,*;q=0.1\";\n    private static final String TEST_ACCEPT_CHARSET = \"utf-8\";\n    private static final String TEST_USER_AGENT_PATTERN = \"^acme4j/.*$\";\n\n    private final URL accountUrl = TestUtils.url(TestUtils.ACCOUNT_URL);\n    private Session session;\n    private Login login;\n    private KeyPair keyPair;\n    private String baseUrl;\n    private URL directoryUrl;\n    private URL newNonceUrl;\n    private URL requestUrl;\n\n    @BeforeEach\n    public void setup(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {\n        baseUrl = wmRuntimeInfo.getHttpBaseUrl();\n        directoryUrl = URI.create(baseUrl + DIRECTORY_PATH).toURL();\n        newNonceUrl = URI.create(baseUrl + NEW_NONCE_PATH).toURL();\n        requestUrl = URI.create(baseUrl + REQUEST_PATH).toURL();\n\n        session = new Session(directoryUrl.toURI());\n        session.setLocale(Locale.JAPAN);\n\n        keyPair = TestUtils.createKeyPair();\n\n        login = session.login(accountUrl, keyPair);\n\n        var directory = new JSONBuilder();\n        directory.put(\"newNonce\", newNonceUrl);\n\n        stubFor(get(DIRECTORY_PATH).willReturn(okJson(directory.toString())));\n    }\n\n    /**\n     * Test that {@link DefaultConnection#getNonce()} is empty if there is no\n     * {@code Replay-Nonce} header.\n     */\n    @Test\n    public void testNoNonceFromHeader() throws AcmeException {\n        stubFor(head(urlEqualTo(NEW_NONCE_PATH)).willReturn(ok()));\n\n        try (var nonceHolder = session.lockNonce(); var conn = session.connect()) {\n            assertThat(nonceHolder.getNonce()).isNull();\n            conn.sendRequest(directoryUrl, session, null);\n            assertThat(conn.getNonce()).isEmpty();\n        }\n    }\n\n    /**\n     * Test that {@link DefaultConnection#getNonce()} extracts a {@code Replay-Nonce}\n     * header correctly.\n     */\n    @Test\n    public void testGetNonceFromHeader() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Replay-Nonce\", TestUtils.DUMMY_NONCE)\n        ));\n\n        try (var nonceHolder = session.lockNonce(); var conn = session.connect()) {\n            assertThat(nonceHolder.getNonce()).isNull();\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getNonce().orElseThrow()).isEqualTo(TestUtils.DUMMY_NONCE);\n            assertThat(nonceHolder.getNonce()).isEqualTo(TestUtils.DUMMY_NONCE);\n        }\n\n        verify(getRequestedFor(urlEqualTo(REQUEST_PATH)));\n    }\n\n    /**\n     * Test that {@link DefaultConnection#getNonce()} handles fails correctly.\n     */\n    @Test\n    public void testGetNonceFromHeaderFailed() throws AcmeException {\n        var retryAfter = Instant.now().plusSeconds(30L).truncatedTo(SECONDS);\n\n        stubFor(head(urlEqualTo(NEW_NONCE_PATH)).willReturn(aResponse()\n                .withStatus(HttpURLConnection.HTTP_UNAVAILABLE)\n                .withHeader(\"Content-Type\", \"application/problem+json\")\n                // do not send a body here because it is a HEAD request!\n        ));\n\n        try (var nonceHolder = session.lockNonce()) {\n            assertThat(nonceHolder.getNonce()).isNull();\n\n            assertThatExceptionOfType(AcmeException.class).isThrownBy(() -> {\n                try (var conn = session.connect()) {\n                    conn.resetNonce(session);\n                }\n            });\n        }\n\n        verify(headRequestedFor(urlEqualTo(NEW_NONCE_PATH)));\n    }\n\n    /**\n     * Test that {@link DefaultConnection#getNonce()} handles a general HTTP error\n     * correctly.\n     */\n    @Test\n    public void testGetNonceFromHeaderHttpError() {\n        stubFor(head(urlEqualTo(NEW_NONCE_PATH)).willReturn(aResponse()\n                .withStatus(HttpURLConnection.HTTP_INTERNAL_ERROR)\n                // do not send a body here because it is a HEAD request!\n        ));\n\n        try (var nonceHolder = session.lockNonce()) {\n            assertThat(nonceHolder.getNonce()).isNull();\n\n            var ex = assertThrows(AcmeException.class, () -> {\n                try (var conn = session.connect()) {\n                    conn.resetNonce(session);\n                }\n            });\n            assertThat(ex.getMessage()).isEqualTo(\"Server responded with HTTP 500 while trying to retrieve a nonce\");\n        }\n\n        verify(headRequestedFor(urlEqualTo(NEW_NONCE_PATH)));\n    }\n\n    /**\n     * Test that {@link DefaultConnection#getNonce()} fails on an invalid\n     * {@code Replay-Nonce} header.\n     */\n    @Test\n    public void testInvalidNonceFromHeader() {\n        var badNonce = \"#$%&/*+*#'\";\n\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Replay-Nonce\", badNonce)\n        ));\n\n        var ex = assertThrows(AcmeProtocolException.class, () -> {\n            try (var conn = session.connect()) {\n                conn.sendRequest(requestUrl, session, null);\n                conn.getNonce();\n            }\n        });\n        assertThat(ex.getMessage()).startsWith(\"Invalid replay nonce\");\n\n        verify(getRequestedFor(urlEqualTo(REQUEST_PATH)));\n    }\n\n    /**\n     * Test that {@link DefaultConnection#resetNonce(Session)} fetches a new nonce via\n     * new-nonce resource and a HEAD request.\n     */\n    @Test\n    public void testResetNonceSucceedsIfNoncePresent() throws AcmeException {\n        stubFor(head(urlEqualTo(NEW_NONCE_PATH)).willReturn(ok()\n                .withHeader(\"Replay-Nonce\", TestUtils.DUMMY_NONCE)\n        ));\n\n        try (var nonceHolder = session.lockNonce(); var conn = session.connect()) {\n            assertThat(nonceHolder.getNonce()).isNull();\n\n            conn.resetNonce(session);\n\n            assertThat(nonceHolder.getNonce()).isEqualTo(TestUtils.DUMMY_NONCE);\n        }\n    }\n\n    /**\n     * Test that {@link DefaultConnection#resetNonce(Session)} throws an exception if\n     * there is no nonce header.\n     */\n    @Test\n    public void testResetNonceThrowsException() {\n        stubFor(head(urlEqualTo(NEW_NONCE_PATH)).willReturn(ok()));\n\n        try (var nonceHolder = session.lockNonce()) {\n            assertThat(nonceHolder.getNonce()).isNull();\n\n            assertThrows(AcmeProtocolException.class, () -> {\n                try (var conn = session.connect()) {\n                    conn.resetNonce(session);\n                }\n            });\n\n            assertThat(nonceHolder.getNonce()).isNull();\n        }\n    }\n\n    /**\n     * Test that an absolute Location header is evaluated.\n     */\n    @Test\n    public void testGetAbsoluteLocation() throws Exception {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Location\", \"https://example.com/otherlocation\")\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            var location = conn.getLocation();\n            assertThat(location).isEqualTo(URI.create(\"https://example.com/otherlocation\").toURL());\n        }\n    }\n\n    /**\n     * Test that a relative Location header is evaluated.\n     */\n    @Test\n    public void testGetRelativeLocation() throws Exception {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Location\", \"/otherlocation\")\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            var location = conn.getLocation();\n            assertThat(location).isEqualTo(URI.create(baseUrl + \"/otherlocation\").toURL());\n        }\n    }\n\n    /**\n     * Test that absolute and relative Link headers are evaluated.\n     */\n    @Test\n    public void testGetLink() throws Exception {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Link\", \"<https://example.com/acme/new-authz>;rel=\\\"next\\\"\")\n                .withHeader(\"Link\", \"</recover-acct>;rel=recover\")\n                .withHeader(\"Link\", \"<https://example.com/acme/terms>; rel=\\\"terms-of-service\\\"\")\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getLinks(\"next\")).containsExactly(URI.create(\"https://example.com/acme/new-authz\").toURL());\n            assertThat(conn.getLinks(\"recover\")).containsExactly(URI.create(baseUrl + \"/recover-acct\").toURL());\n            assertThat(conn.getLinks(\"terms-of-service\")).containsExactly(URI.create(\"https://example.com/acme/terms\").toURL());\n            assertThat(conn.getLinks(\"secret-stuff\")).isEmpty();\n        }\n    }\n\n    /**\n     * Test that multiple link headers are evaluated.\n     */\n    @Test\n    public void testGetMultiLink() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Link\", \"<https://example.com/acme/terms1>; rel=\\\"terms-of-service\\\"\")\n                .withHeader(\"Link\", \"<https://example.com/acme/terms2>; rel=\\\"terms-of-service\\\"\")\n                .withHeader(\"Link\", \"<../terms3>; rel=\\\"terms-of-service\\\"\")\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getLinks(\"terms-of-service\")).containsExactlyInAnyOrder(\n                    url(\"https://example.com/acme/terms1\"),\n                    url(\"https://example.com/acme/terms2\"),\n                    url(baseUrl + \"/terms3\")\n            );\n        }\n    }\n\n    /**\n     * Test that link headers with multiple header fields are evaluated\n     */\n    @Test\n    public void testGetMultiHeaderFieldLink() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Link\", \"<https://example.com/acme/terms1>; rel=\\\"terms-of-service\\\"; title=\\\"Please Read\\\"\")\n                .withHeader(\"Link\", \"<https://example.com/acme/terms2>; title=\\\"Please read and accept\\\"; rel=\\\"terms-of-service\\\"\")\n                .withHeader(\"Link\", \"<../terms3>;  anchor=\\\"foo\\\";  rel=\\\"terms-of-service\\\" ; title=\\\"More ToS to read\\\"\")\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getLinks(\"terms-of-service\")).containsExactlyInAnyOrder(\n                    url(\"https://example.com/acme/terms1\"),\n                    url(\"https://example.com/acme/terms2\"),\n                    url(baseUrl + \"/terms3\")\n            );\n        }\n    }\n\n    /**\n     * Test that no link headers are properly handled.\n     */\n    @Test\n    public void testGetNoLink() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getLinks(\"something\")).isEmpty();\n        }\n    }\n\n    /**\n     * Test that no Location header returns {@code null}.\n     */\n    @Test\n    public void testNoLocation() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThatExceptionOfType(AcmeProtocolException.class)\n                    .isThrownBy(conn::getLocation);\n        }\n\n        verify(getRequestedFor(urlEqualTo(REQUEST_PATH)));\n    }\n\n    /**\n     * Test if Retry-After header with absolute date is correctly parsed.\n     */\n    @Test\n    public void testHandleRetryAfterHeaderDate() throws AcmeException {\n        var retryDate = Instant.now().plus(Duration.ofHours(10)).truncatedTo(SECONDS);\n\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Retry-After\", DATE_FORMATTER.format(retryDate))\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getRetryAfter()).hasValue(retryDate);\n        }\n    }\n\n    /**\n     * Test if Retry-After header with relative timespan is correctly parsed.\n     */\n    @Test\n    public void testHandleRetryAfterHeaderDelta() throws AcmeException {\n        var delta = 10 * 60 * 60;\n        var now = Instant.now().truncatedTo(SECONDS);\n        var retryMsg = \"relative time\";\n\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Retry-After\", String.valueOf(delta))\n                .withHeader(\"Date\", DATE_FORMATTER.format(now))\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getRetryAfter()).hasValue(now.plusSeconds(delta));\n        }\n    }\n\n    /**\n     * Test if no Retry-After header is correctly handled.\n     */\n    @Test\n    public void testHandleRetryAfterHeaderNull() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Date\", DATE_FORMATTER.format(Instant.now()))\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getRetryAfter()).isEmpty();\n        }\n\n        verify(getRequestedFor(urlEqualTo(REQUEST_PATH)));\n    }\n\n    /**\n     * Test if no exception is thrown on a standard request.\n     */\n    @Test\n    public void testAccept() throws AcmeException {\n        stubFor(post(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withBody(\"\")\n        ));\n\n        try (var nonceHolder = session.lockNonce()) {\n            nonceHolder.setNonce(TestUtils.DUMMY_NONCE);\n        }\n\n        try (var conn = session.connect()) {\n            var rc = conn.sendSignedRequest(requestUrl, new JSONBuilder(), login);\n            assertThat(rc).isEqualTo(HttpURLConnection.HTTP_OK);\n        }\n\n        verify(postRequestedFor(urlEqualTo(REQUEST_PATH)));\n    }\n\n    /**\n     * Test if an {@link AcmeServerException} is thrown on an acme problem.\n     */\n    @Test\n    public void testAcceptThrowsException() {\n        var problem = new JSONBuilder();\n        problem.put(\"type\", \"urn:ietf:params:acme:error:unauthorized\");\n        problem.put(\"detail\", \"Invalid response: 404\");\n\n        stubFor(post(urlEqualTo(REQUEST_PATH)).willReturn(aResponse()\n                .withStatus(HttpURLConnection.HTTP_FORBIDDEN)\n                .withHeader(\"Content-Type\", \"application/problem+json\")\n                .withBody(problem.toString())\n        ));\n\n        try (var nonceHolder = session.lockNonce()) {\n            nonceHolder.setNonce(TestUtils.DUMMY_NONCE);\n        }\n\n        var ex = assertThrows(AcmeException.class, () -> {\n            try (var conn = session.connect()) {\n                conn.sendSignedRequest(requestUrl, new JSONBuilder(), login);\n            }\n        });\n\n        assertThat(ex).isInstanceOf(AcmeUnauthorizedException.class);\n        assertThat(((AcmeUnauthorizedException) ex).getType())\n                .isEqualTo(URI.create(\"urn:ietf:params:acme:error:unauthorized\"));\n        assertThat(ex.getMessage()).isEqualTo(\"Invalid response: 404\");\n    }\n\n    /**\n     * Test if an {@link AcmeUserActionRequiredException} is thrown on an acme problem.\n     */\n    @Test\n    public void testAcceptThrowsUserActionRequiredException() {\n        var problem = new JSONBuilder();\n        problem.put(\"type\", \"urn:ietf:params:acme:error:userActionRequired\");\n        problem.put(\"detail\", \"Accept the TOS\");\n\n        stubFor(post(urlEqualTo(REQUEST_PATH)).willReturn(aResponse()\n                .withStatus(HttpURLConnection.HTTP_FORBIDDEN)\n                .withHeader(\"Content-Type\", \"application/problem+json\")\n                .withHeader(\"Link\", \"<https://example.com/tos.pdf>; rel=\\\"terms-of-service\\\"\")\n                .withBody(problem.toString())\n        ));\n\n        try (var nonceHolder = session.lockNonce()) {\n            nonceHolder.setNonce(TestUtils.DUMMY_NONCE);\n        }\n\n        var ex = assertThrows(AcmeException.class, () -> {\n            try (var conn = session.connect()) {\n                conn.sendSignedRequest(requestUrl, new JSONBuilder(), login);\n            }\n        });\n\n        assertThat(ex).isInstanceOf(AcmeUserActionRequiredException.class);\n        assertThat(((AcmeUserActionRequiredException) ex).getType())\n                .isEqualTo(URI.create(\"urn:ietf:params:acme:error:userActionRequired\"));\n        assertThat(ex.getMessage()).isEqualTo(\"Accept the TOS\");\n        assertThat(((AcmeUserActionRequiredException) ex).getTermsOfServiceUri().orElseThrow())\n                .isEqualTo(URI.create(\"https://example.com/tos.pdf\"));\n    }\n\n    /**\n     * Test if an {@link AcmeRateLimitedException} is thrown on an acme problem.\n     */\n    @Test\n    public void testAcceptThrowsRateLimitedException() {\n        var problem = new JSONBuilder();\n        problem.put(\"type\", \"urn:ietf:params:acme:error:rateLimited\");\n        problem.put(\"detail\", \"Too many invocations\");\n\n        var retryAfter = Instant.now().plusSeconds(30L).truncatedTo(SECONDS);\n\n        stubFor(post(urlEqualTo(REQUEST_PATH)).willReturn(aResponse()\n                .withStatus(HttpURLConnection.HTTP_FORBIDDEN)\n                .withHeader(\"Content-Type\", \"application/problem+json\")\n                .withHeader(\"Link\", \"<https://example.com/rates.pdf>; rel=\\\"help\\\"\")\n                .withHeader(\"Retry-After\", DATE_FORMATTER.format(retryAfter))\n                .withBody(problem.toString())\n        ));\n\n        try (var nonceHolder = session.lockNonce()) {\n            nonceHolder.setNonce(TestUtils.DUMMY_NONCE);\n        }\n\n        var ex = assertThrows(AcmeRateLimitedException.class, () -> {\n            try (var conn = session.connect()) {\n                conn.sendSignedRequest(requestUrl, new JSONBuilder(), login);\n            }\n        });\n\n        assertThat(ex.getType()).isEqualTo(URI.create(\"urn:ietf:params:acme:error:rateLimited\"));\n        assertThat(ex.getMessage()).isEqualTo(\"Too many invocations\");\n        assertThat(ex.getRetryAfter().orElseThrow()).isEqualTo(retryAfter);\n        assertThat(ex.getDocuments()).isNotNull();\n        assertThat(ex.getDocuments()).hasSize(1);\n        assertThat(ex.getDocuments().iterator().next()).isEqualTo(url(\"https://example.com/rates.pdf\"));\n    }\n\n    /**\n     * Test if an {@link AcmeServerException} is thrown on another problem.\n     */\n    @Test\n    public void testAcceptThrowsOtherException() {\n        var problem = new JSONBuilder();\n        problem.put(\"type\", \"urn:zombie:error:apocalypse\");\n        problem.put(\"detail\", \"Zombie apocalypse in progress\");\n\n        stubFor(post(urlEqualTo(REQUEST_PATH)).willReturn(aResponse()\n                .withStatus(HttpURLConnection.HTTP_INTERNAL_ERROR)\n                .withHeader(\"Content-Type\", \"application/problem+json\")\n                .withBody(problem.toString())\n        ));\n\n        try (var nonceHolder = session.lockNonce()) {\n            nonceHolder.setNonce(TestUtils.DUMMY_NONCE);\n        }\n\n        var ex = assertThrows(AcmeServerException.class, () -> {\n            try (var conn = session.connect()) {\n                conn.sendSignedRequest(requestUrl, new JSONBuilder(), login);\n            }\n        });\n\n        assertThat(ex.getType()).isEqualTo(URI.create(\"urn:zombie:error:apocalypse\"));\n        assertThat(ex.getMessage()).isEqualTo(\"Zombie apocalypse in progress\");\n    }\n\n    /**\n     * Test if an {@link AcmeException} is thrown if there is no error type.\n     */\n    @Test\n    public void testAcceptThrowsNoTypeException() {\n        stubFor(post(urlEqualTo(REQUEST_PATH)).willReturn(aResponse()\n                .withStatus(HttpURLConnection.HTTP_INTERNAL_ERROR)\n                .withHeader(\"Content-Type\", \"application/problem+json\")\n                .withBody(\"{}\")\n        ));\n\n        try (var nonceHolder = session.lockNonce()) {\n            nonceHolder.setNonce(TestUtils.DUMMY_NONCE);\n        }\n\n        var ex = assertThrows(AcmeProtocolException.class, () -> {\n            try (var conn = session.connect()) {\n                conn.sendSignedRequest(requestUrl, new JSONBuilder(), login);\n            }\n        });\n        assertThat(ex.getMessage()).isNotEmpty();\n    }\n\n    /**\n     * Test if an {@link AcmeException} is thrown if there is a generic error.\n     */\n    @Test\n    public void testAcceptThrowsServerException() {\n        stubFor(post(urlEqualTo(REQUEST_PATH)).willReturn(aResponse()\n                .withStatus(HttpURLConnection.HTTP_INTERNAL_ERROR)\n                .withStatusMessage(\"Infernal Server Error\")\n                .withHeader(\"Content-Type\", \"text/html\")\n                .withBody(\"<html><head><title>Infernal Server Error</title></head></html>\")\n        ));\n\n        try (var nonceHolder = session.lockNonce()) {\n            nonceHolder.setNonce(TestUtils.DUMMY_NONCE);\n        }\n\n        var ex = assertThrows(AcmeException.class, () -> {\n            try (var conn = session.connect()) {\n                conn.sendSignedRequest(requestUrl, new JSONBuilder(), login);\n            }\n        });\n        assertThat(ex.getMessage()).isEqualTo(\"HTTP 500\");\n    }\n\n    /**\n     * Test GET requests.\n     */\n    @Test\n    public void testSendRequest() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n        }\n\n        verify(getRequestedFor(urlEqualTo(REQUEST_PATH))\n                .withHeader(\"Accept\", equalTo(\"application/json\"))\n                .withHeader(\"Accept-Charset\", equalTo(TEST_ACCEPT_CHARSET))\n                .withHeader(\"Accept-Language\", equalTo(TEST_ACCEPT_LANGUAGE))\n                .withHeader(\"User-Agent\", matching(TEST_USER_AGENT_PATTERN))\n        );\n    }\n\n    /**\n     * Test GET requests with If-Modified-Since.\n     */\n    @Test\n    public void testSendRequestIfModifiedSince() throws AcmeException {\n        var ifModifiedSince = ZonedDateTime.now(ZoneId.of(\"UTC\")).truncatedTo(SECONDS);\n\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(aResponse()\n                .withStatus(HttpURLConnection.HTTP_NOT_MODIFIED))\n        );\n\n        try (var conn = session.connect()) {\n            var rc = conn.sendRequest(requestUrl, session, ifModifiedSince);\n            assertThat(rc).isEqualTo(HttpURLConnection.HTTP_NOT_MODIFIED);\n        }\n\n        verify(getRequestedFor(urlEqualTo(REQUEST_PATH))\n                .withHeader(\"If-Modified-Since\", equalToDateTime(ifModifiedSince))\n                .withHeader(\"Accept\", equalTo(\"application/json\"))\n                .withHeader(\"Accept-Charset\", equalTo(TEST_ACCEPT_CHARSET))\n                .withHeader(\"Accept-Language\", equalTo(TEST_ACCEPT_LANGUAGE))\n                .withHeader(\"User-Agent\", matching(TEST_USER_AGENT_PATTERN))\n        );\n    }\n\n    /**\n     * Test signed POST requests.\n     */\n    @Test\n    public void testSendSignedRequest() throws Exception {\n        var nonce1 = URL_ENCODER.encodeToString(\"foo-nonce-1-foo\".getBytes());\n        var nonce2 = URL_ENCODER.encodeToString(\"foo-nonce-2-foo\".getBytes());\n\n        stubFor(head(urlEqualTo(NEW_NONCE_PATH)).willReturn(ok()\n                .withHeader(\"Replay-Nonce\", nonce1)));\n\n        stubFor(post(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Replay-Nonce\", nonce2)\n        ));\n\n        try (var conn = session.connect()) {\n            var cb = new JSONBuilder();\n            cb.put(\"foo\", 123).put(\"bar\", \"a-string\");\n            conn.sendSignedRequest(requestUrl, cb, login);\n        }\n\n        try (var nonceHolder = session.lockNonce()) {\n            assertThat(nonceHolder.getNonce()).isEqualTo(nonce2);\n        }\n\n        verify(postRequestedFor(urlEqualTo(REQUEST_PATH))\n                .withHeader(\"Accept\", equalTo(\"application/json\"))\n                .withHeader(\"Accept-Charset\", equalTo(TEST_ACCEPT_CHARSET))\n                .withHeader(\"Accept-Language\", equalTo(TEST_ACCEPT_LANGUAGE))\n                .withHeader(\"User-Agent\", matching(TEST_USER_AGENT_PATTERN))\n        );\n\n        var requests = findAll(postRequestedFor(urlEqualTo(REQUEST_PATH)));\n        assertThat(requests).hasSize(1);\n\n        var data = JSON.parse(requests.get(0).getBodyAsString());\n        var encodedHeader = data.get(\"protected\").asString();\n        var encodedSignature = data.get(\"signature\").asString();\n        var encodedPayload = data.get(\"payload\").asString();\n\n        var expectedHeader = new StringBuilder();\n        expectedHeader.append('{');\n        expectedHeader.append(\"\\\"nonce\\\":\\\"\").append(nonce1).append(\"\\\",\");\n        expectedHeader.append(\"\\\"url\\\":\\\"\").append(requestUrl).append(\"\\\",\");\n        expectedHeader.append(\"\\\"alg\\\":\\\"RS256\\\",\");\n        expectedHeader.append(\"\\\"kid\\\":\\\"\").append(accountUrl).append('\"');\n        expectedHeader.append('}');\n\n        assertThatJson(new String(URL_DECODER.decode(encodedHeader), UTF_8)).isEqualTo(expectedHeader.toString());\n        assertThatJson(new String(URL_DECODER.decode(encodedPayload), UTF_8)).isEqualTo(\"{\\\"foo\\\":123,\\\"bar\\\":\\\"a-string\\\"}\");\n        assertThat(encodedSignature).isNotEmpty();\n\n        var jws = new JsonWebSignature();\n        jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature));\n        jws.setKey(login.getPublicKey());\n        assertThat(jws.verifySignature()).isTrue();\n    }\n\n    /**\n     * Test signed POST-as-GET requests.\n     */\n    @Test\n    public void testSendSignedPostAsGetRequest() throws Exception {\n        var nonce1 = URL_ENCODER.encodeToString(\"foo-nonce-1-foo\".getBytes());\n        var nonce2 = URL_ENCODER.encodeToString(\"foo-nonce-2-foo\".getBytes());\n\n        stubFor(head(urlEqualTo(NEW_NONCE_PATH)).willReturn(ok()\n                .withHeader(\"Replay-Nonce\", nonce1)));\n\n        stubFor(post(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Replay-Nonce\", nonce2)));\n\n        try (var conn = session.connect()) {\n            conn.sendSignedPostAsGetRequest(requestUrl, login);\n        }\n\n        try (var nonceHolder = session.lockNonce()) {\n            assertThat(nonceHolder.getNonce()).isEqualTo(nonce2);\n        }\n\n        verify(postRequestedFor(urlEqualTo(REQUEST_PATH))\n                .withHeader(\"Accept\", equalTo(\"application/json\"))\n                .withHeader(\"Accept-Charset\", equalTo(TEST_ACCEPT_CHARSET))\n                .withHeader(\"Accept-Language\", equalTo(TEST_ACCEPT_LANGUAGE))\n                .withHeader(\"Content-Type\", equalTo(\"application/jose+json\"))\n                .withHeader(\"User-Agent\", matching(TEST_USER_AGENT_PATTERN))\n        );\n\n        var requests = findAll(postRequestedFor(urlEqualTo(REQUEST_PATH)));\n        assertThat(requests).hasSize(1);\n\n        var data = JSON.parse(requests.get(0).getBodyAsString());\n        var encodedHeader = data.get(\"protected\").asString();\n        var encodedSignature = data.get(\"signature\").asString();\n        var encodedPayload = data.get(\"payload\").asString();\n\n        var expectedHeader = new StringBuilder();\n        expectedHeader.append('{');\n        expectedHeader.append(\"\\\"nonce\\\":\\\"\").append(nonce1).append(\"\\\",\");\n        expectedHeader.append(\"\\\"url\\\":\\\"\").append(requestUrl).append(\"\\\",\");\n        expectedHeader.append(\"\\\"alg\\\":\\\"RS256\\\",\");\n        expectedHeader.append(\"\\\"kid\\\":\\\"\").append(accountUrl).append('\"');\n        expectedHeader.append('}');\n\n        assertThatJson(new String(URL_DECODER.decode(encodedHeader), UTF_8)).isEqualTo(expectedHeader.toString());\n        assertThat(new String(URL_DECODER.decode(encodedPayload), UTF_8)).isEqualTo(\"\");\n        assertThat(encodedSignature).isNotEmpty();\n\n        var jws = new JsonWebSignature();\n        jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature));\n        jws.setKey(login.getPublicKey());\n        assertThat(jws.verifySignature()).isTrue();\n    }\n\n    /**\n     * Test certificate POST-as-GET requests.\n     */\n    @Test\n    public void testSendCertificateRequest() throws AcmeException {\n        var nonce1 = URL_ENCODER.encodeToString(\"foo-nonce-1-foo\".getBytes());\n        var nonce2 = URL_ENCODER.encodeToString(\"foo-nonce-2-foo\".getBytes());\n\n        stubFor(head(urlEqualTo(NEW_NONCE_PATH)).willReturn(ok()\n                .withHeader(\"Replay-Nonce\", nonce1)));\n\n        stubFor(post(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Replay-Nonce\", nonce2)));\n\n        try (var conn = session.connect()) {\n            conn.sendCertificateRequest(requestUrl, login);\n        }\n\n        try (var nonceHolder = session.lockNonce()) {\n            assertThat(nonceHolder.getNonce()).isEqualTo(nonce2);\n        }\n\n        verify(postRequestedFor(urlEqualTo(REQUEST_PATH))\n                .withHeader(\"Accept\", equalTo(\"application/pem-certificate-chain\"))\n                .withHeader(\"Accept-Charset\", equalTo(TEST_ACCEPT_CHARSET))\n                .withHeader(\"Accept-Language\", equalTo(TEST_ACCEPT_LANGUAGE))\n                .withHeader(\"Content-Type\", equalTo(\"application/jose+json\"))\n                .withHeader(\"User-Agent\", matching(TEST_USER_AGENT_PATTERN))\n        );\n    }\n\n    /**\n     * Test signed POST requests without KeyIdentifier.\n     */\n    @Test\n    public void testSendSignedRequestNoKid() throws Exception {\n        var nonce1 = URL_ENCODER.encodeToString(\"foo-nonce-1-foo\".getBytes());\n        var nonce2 = URL_ENCODER.encodeToString(\"foo-nonce-2-foo\".getBytes());\n\n        stubFor(head(urlEqualTo(NEW_NONCE_PATH)).willReturn(ok()\n                .withHeader(\"Replay-Nonce\", nonce1)));\n\n        stubFor(post(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Replay-Nonce\", nonce2)));\n\n        try (var conn = session.connect()) {\n            var cb = new JSONBuilder();\n            cb.put(\"foo\", 123).put(\"bar\", \"a-string\");\n            conn.sendSignedRequest(requestUrl, cb, session,\n                    (url, payload, nonce) -> JoseUtils.createJoseRequest(url, keyPair, payload, nonce, null));\n        }\n\n        try (var nonceHolder = session.lockNonce()) {\n            assertThat(nonceHolder.getNonce()).isEqualTo(nonce2);\n        }\n\n        verify(postRequestedFor(urlEqualTo(REQUEST_PATH))\n                .withHeader(\"Accept\", equalTo(\"application/json\"))\n                .withHeader(\"Accept-Charset\", equalTo(TEST_ACCEPT_CHARSET))\n                .withHeader(\"Accept-Language\", equalTo(TEST_ACCEPT_LANGUAGE))\n                .withHeader(\"Content-Type\", equalTo(\"application/jose+json\"))\n                .withHeader(\"User-Agent\", matching(TEST_USER_AGENT_PATTERN))\n        );\n\n        var requests = findAll(postRequestedFor(urlEqualTo(REQUEST_PATH)));\n        assertThat(requests).hasSize(1);\n\n        var data = JSON.parse(requests.get(0).getBodyAsString());\n        String encodedHeader = data.get(\"protected\").asString();\n        String encodedSignature = data.get(\"signature\").asString();\n        String encodedPayload = data.get(\"payload\").asString();\n\n        var expectedHeader = new StringBuilder();\n        expectedHeader.append('{');\n        expectedHeader.append(\"\\\"nonce\\\":\\\"\").append(nonce1).append(\"\\\",\");\n        expectedHeader.append(\"\\\"url\\\":\\\"\").append(requestUrl).append(\"\\\",\");\n        expectedHeader.append(\"\\\"alg\\\":\\\"RS256\\\",\");\n        expectedHeader.append(\"\\\"jwk\\\":{\");\n        expectedHeader.append(\"\\\"kty\\\":\\\"\").append(TestUtils.KTY).append(\"\\\",\");\n        expectedHeader.append(\"\\\"e\\\":\\\"\").append(TestUtils.E).append(\"\\\",\");\n        expectedHeader.append(\"\\\"n\\\":\\\"\").append(TestUtils.N).append(\"\\\"\");\n        expectedHeader.append(\"}}\");\n\n        assertThatJson(new String(URL_DECODER.decode(encodedHeader), UTF_8))\n                .isEqualTo(expectedHeader.toString());\n        assertThatJson(new String(URL_DECODER.decode(encodedPayload), UTF_8))\n                .isEqualTo(\"{\\\"foo\\\":123,\\\"bar\\\":\\\"a-string\\\"}\");\n        assertThat(encodedSignature).isNotEmpty();\n\n        var jws = new JsonWebSignature();\n        jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature));\n        jws.setKey(login.getPublicKey());\n        assertThat(jws.verifySignature()).isTrue();\n    }\n\n    /**\n     * Test signed POST requests if there is no nonce.\n     */\n    @Test\n    public void testSendSignedRequestNoNonce() {\n        stubFor(head(urlEqualTo(NEW_NONCE_PATH)).willReturn(notFound()));\n\n        assertThrows(AcmeException.class, () -> {\n            try (var conn = session.connect()) {\n                conn.sendSignedRequest(requestUrl, new JSONBuilder(), session,\n                        (url, payload, nonce) -> JoseUtils.createJoseRequest(url, keyPair, payload, nonce, null));\n            }\n        });\n    }\n\n    /**\n     * Test getting a JSON response.\n     */\n    @Test\n    public void testReadJsonResponse() throws AcmeException {\n        var response = new JSONBuilder();\n        response.put(\"foo\", 123);\n        response.put(\"bar\", \"a-string\");\n\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Content-Type\", \"application/json\")\n                .withBody(response.toString())\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n\n            var result = conn.readJsonResponse();\n            assertThat(result).isNotNull();\n            assertThat(result.keySet()).hasSize(2);\n            assertThat(result.get(\"foo\").asInt()).isEqualTo(123);\n            assertThat(result.get(\"bar\").asString()).isEqualTo(\"a-string\");\n        }\n    }\n\n    /**\n     * Test that a certificate is downloaded correctly.\n     */\n    @Test\n    public void testReadCertificate() throws Exception {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Content-Type\", \"application/pem-certificate-chain\")\n                .withBody(getResourceAsByteArray(\"/cert.pem\"))\n        ));\n\n        List<X509Certificate> downloaded;\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            downloaded = conn.readCertificates();\n        }\n\n        var original = TestUtils.createCertificate(\"/cert.pem\");\n        assertThat(original).hasSize(2);\n\n        assertThat(downloaded).isNotNull();\n        assertThat(downloaded).hasSize(original.size());\n        for (var ix = 0; ix < downloaded.size(); ix++) {\n            assertThat(downloaded.get(ix).getEncoded()).isEqualTo(original.get(ix).getEncoded());\n        }\n    }\n\n    /**\n     * Test that a bad certificate throws an exception.\n     */\n    @Test\n    public void testReadBadCertificate() throws Exception {\n        // Build a broken certificate chain PEM file\n        byte[] brokenPem;\n        try (var baos = new ByteArrayOutputStream(); var w = new OutputStreamWriter(baos)) {\n            for (var cert : TestUtils.createCertificate(\"/cert.pem\")) {\n                var badCert = cert.getEncoded();\n                Arrays.sort(badCert); // break it\n                AcmeUtils.writeToPem(badCert, AcmeUtils.PemLabel.CERTIFICATE, w);\n            }\n            w.flush();\n            brokenPem = baos.toByteArray();\n        }\n\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Content-Type\", \"application/pem-certificate-chain\")\n                .withBody(brokenPem)\n        ));\n\n        assertThrows(AcmeProtocolException.class, () -> {\n            try (var conn = session.connect()) {\n                conn.sendRequest(requestUrl, session, null);\n                conn.readCertificates();\n            }\n        });\n    }\n\n    /**\n     * Test that {@link DefaultConnection#getLastModified()} returns valid dates.\n     */\n    @Test\n    public void testLastModifiedUnset() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getLastModified().isPresent()).isFalse();\n        }\n    }\n\n    @Test\n    public void testLastModifiedSet() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Last-Modified\", \"Thu, 07 May 2020 19:42:46 GMT\")\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n\n            var lm = conn.getLastModified();\n            assertThat(lm.isPresent()).isTrue();\n            assertThat(lm.get().format(DateTimeFormatter.ISO_DATE_TIME))\n                    .isEqualTo(\"2020-05-07T19:42:46Z\");\n        }\n    }\n\n    @Test\n    public void testLastModifiedInvalid() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Last-Modified\", \"iNvAlId\")\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getLastModified().isPresent()).isFalse();\n        }\n    }\n\n    /**\n     * Test that {@link DefaultConnection#getExpiration()} returns valid dates.\n     */\n    @Test\n    public void testExpirationUnset() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getExpiration().isPresent()).isFalse();\n        }\n    }\n\n    @Test\n    public void testExpirationNoCache() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Cache-Control\", \"public, no-cache\")\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getExpiration().isPresent()).isFalse();\n        }\n    }\n\n    @Test\n    public void testExpirationMaxAgeZero() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Cache-Control\", \"public, max-age=0, no-cache\")\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getExpiration().isPresent()).isFalse();\n        }\n    }\n\n    @Test\n    public void testExpirationMaxAgeButNoCache() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Cache-Control\", \"public, max-age=3600, no-cache\")\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getExpiration().isPresent()).isFalse();\n        }\n    }\n\n    @Test\n    public void testExpirationMaxAge() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Cache-Control\", \"max-age=3600\")\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n\n            var exp = conn.getExpiration();\n            assertThat(exp.isPresent()).isTrue();\n            assertThat(exp.get().isAfter(ZonedDateTime.now().plusHours(1).minusMinutes(1))).isTrue();\n            assertThat(exp.get().isBefore(ZonedDateTime.now().plusHours(1).plusMinutes(1))).isTrue();\n        }\n    }\n\n    @Test\n    public void testExpirationExpires() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Expires\", \"Thu, 18 Jun 2020 08:43:04 GMT\")\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n\n            var exp = conn.getExpiration();\n            assertThat(exp.isPresent()).isTrue();\n            assertThat(exp.get().format(DateTimeFormatter.ISO_DATE_TIME))\n                    .isEqualTo(\"2020-06-18T08:43:04Z\");\n        }\n    }\n\n    @Test\n    public void testExpirationInvalidExpires() throws AcmeException {\n        stubFor(get(urlEqualTo(REQUEST_PATH)).willReturn(ok()\n                .withHeader(\"Expires\", \"iNvAlId\")\n        ));\n\n        try (var conn = session.connect()) {\n            conn.sendRequest(requestUrl, session, null);\n            assertThat(conn.getExpiration().isPresent()).isFalse();\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/connector/DummyConnection.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport java.net.URL;\nimport java.security.cert.X509Certificate;\nimport java.time.Instant;\nimport java.time.ZonedDateTime;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Optional;\n\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\n\n/**\n * Dummy implementation of {@link Connection} that always fails. Single methods are\n * supposed to be overridden for testing.\n */\npublic class DummyConnection implements Connection {\n\n    @Override\n    public void resetNonce(Session session) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public int sendRequest(URL url, Session session, ZonedDateTime ifModifiedSince) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public int sendCertificateRequest(URL url, Login login) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public int sendSignedPostAsGetRequest(URL url, Login login) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public int sendSignedRequest(URL url, JSONBuilder claims, Login login)\n                throws AcmeException {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public int sendSignedRequest(URL url, JSONBuilder claims, Session session, RequestSigner signer) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public JSON readJsonResponse() {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public List<X509Certificate> readCertificates() {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Optional<Instant> getRetryAfter() {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Optional<String> getNonce() {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public URL getLocation() {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Optional<ZonedDateTime> getLastModified() {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Optional<ZonedDateTime> getExpiration() {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Collection<URL> getLinks(String relation) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public void close() {\n        // closing is always safe\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/connector/HttpConnectorTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link HttpConnector}.\n */\npublic class HttpConnectorTest {\n\n    /**\n     * Test if a {@link java.net.http.HttpRequest.Builder} can be created and has proper\n     * default values.\n     */\n    @Test\n    public void testRequestBuilderDefaultValues() throws Exception {\n        var url = URI.create(\"http://example.org:123/foo\").toURL();\n        var settings = new NetworkSettings();\n        var httpClient = HttpClient.newBuilder().build();\n\n        var connector = new HttpConnector(settings, httpClient);\n        var request = connector.createRequestBuilder(url).build();\n\n        assertThat(request.uri().toString()).isEqualTo(url.toExternalForm());\n        assertThat(request.timeout().orElseThrow()).isEqualTo(settings.getTimeout());\n        assertThat(request.headers().firstValue(\"User-Agent\").orElseThrow())\n                .isEqualTo(HttpConnector.defaultUserAgent());\n    }\n\n    /**\n     * Tests that the user agent is correct.\n     */\n    @Test\n    public void testUserAgent() {\n        var userAgent = HttpConnector.defaultUserAgent();\n        assertThat(userAgent).contains(\"acme4j/\");\n        assertThat(userAgent).contains(\"Java/\");\n    }\n\n    /**\n     * Test that getHttpClient returns the HttpClient passed to the constructor.\n     */\n    @Test\n    public void testGetHttpClient() {\n        var settings = new NetworkSettings();\n        var httpClient = HttpClient.newBuilder().build();\n\n        var connector = new HttpConnector(settings, httpClient);\n\n        // Should return the same client instance that was passed in\n        assertThat(connector.getHttpClient()).isSameAs(httpClient);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/connector/NetworkSettingsTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2019 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.net.Authenticator;\nimport java.net.InetSocketAddress;\nimport java.net.ProxySelector;\nimport java.net.http.HttpClient;\nimport java.time.Duration;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link NetworkSettings}.\n */\npublic class NetworkSettingsTest {\n\n    /**\n     * Test getters and setters.\n     */\n    @Test\n    public void testGettersAndSetters() {\n        var settings = new NetworkSettings();\n\n        var proxyAddress = new InetSocketAddress(\"198.51.100.1\", 8080);\n        var proxySelector = ProxySelector.of(proxyAddress);\n\n        assertThat(settings.getProxySelector()).isSameAs(HttpClient.Builder.NO_PROXY);\n        settings.setProxySelector(proxySelector);\n        assertThat(settings.getProxySelector()).isSameAs(proxySelector);\n        settings.setProxySelector(null);\n        assertThat(settings.getProxySelector()).isEqualTo(HttpClient.Builder.NO_PROXY);\n\n        assertThat(settings.getTimeout()).isEqualTo(Duration.ofSeconds(30));\n        settings.setTimeout(Duration.ofMillis(5120));\n        assertThat(settings.getTimeout()).isEqualTo(Duration.ofMillis(5120));\n\n        var defaultAuthenticator = Authenticator.getDefault();\n        assertThat(settings.getAuthenticator()).isNull();\n        settings.setAuthenticator(defaultAuthenticator);\n        assertThat(settings.getAuthenticator()).isSameAs(defaultAuthenticator);\n\n        assertThat(settings.isCompressionEnabled()).isTrue();\n        settings.setCompressionEnabled(false);\n        assertThat(settings.isCompressionEnabled()).isFalse();\n    }\n\n    @Test\n    public void testInvalidTimeouts() {\n        var settings = new NetworkSettings();\n\n        assertThrows(IllegalArgumentException.class,\n                () -> settings.setTimeout(null),\n                \"timeout accepted null\");\n        assertThrows(IllegalArgumentException.class,\n                () -> settings.setTimeout(Duration.ZERO),\n                \"timeout accepted zero duration\");\n        assertThrows(IllegalArgumentException.class,\n                () -> settings.setTimeout(Duration.ofSeconds(20).negated()),\n                \"timeout accepted negative duration\");\n    }\n\n    @Test\n    public void testSystemProperty() {\n        assertThat(NetworkSettings.GZIP_PROPERTY_NAME)\n                .startsWith(\"org.shredzone.acme4j\")\n                .contains(\"gzip\");\n\n        System.clearProperty(NetworkSettings.GZIP_PROPERTY_NAME);\n        var settingsNone = new NetworkSettings();\n        assertThat(settingsNone.isCompressionEnabled()).isTrue();\n\n        System.setProperty(NetworkSettings.GZIP_PROPERTY_NAME, \"true\");\n        var settingsTrue = new NetworkSettings();\n        assertThat(settingsTrue.isCompressionEnabled()).isTrue();\n\n        System.setProperty(NetworkSettings.GZIP_PROPERTY_NAME, \"false\");\n        var settingsFalse = new NetworkSettings();\n        assertThat(settingsFalse.isCompressionEnabled()).isFalse();\n\n        System.setProperty(NetworkSettings.GZIP_PROPERTY_NAME, \"1234\");\n        var settingsNonBoolean = new NetworkSettings();\n        assertThat(settingsNonBoolean.isCompressionEnabled()).isFalse();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceIteratorTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.io.IOException;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.NoSuchElementException;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Authorization;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.provider.TestableConnectionProvider;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\n\n/**\n * Unit test for {@link ResourceIterator}.\n */\npublic class ResourceIteratorTest {\n\n    private static final int PAGES = 4;\n    private static final int RESOURCES_PER_PAGE = 5;\n    private static final String TYPE = \"authorizations\";\n\n    private final List<URL> resourceURLs = new ArrayList<>(PAGES * RESOURCES_PER_PAGE);\n    private final List<URL> pageURLs = new ArrayList<>(PAGES);\n\n    @BeforeEach\n    public void setup() {\n        resourceURLs.clear();\n        for (var ix = 0; ix < RESOURCES_PER_PAGE * PAGES; ix++) {\n            resourceURLs.add(url(\"https://example.com/acme/auth/\" + ix));\n        }\n\n        pageURLs.clear();\n        for (var ix = 0; ix < PAGES; ix++) {\n            pageURLs.add(url(\"https://example.com/acme/batch/\" + ix));\n        }\n    }\n\n    /**\n     * Test if the {@link ResourceIterator} handles a {@code null} start URL.\n     */\n    @Test\n    public void nullTest() {\n        assertThrows(NoSuchElementException.class, () -> {\n            var it = createIterator(null);\n\n            assertThat(it).isNotNull();\n            assertThat(it.hasNext()).isFalse();\n            it.next(); // throws NoSuchElementException\n        });\n    }\n\n    /**\n     * Test if the {@link ResourceIterator} returns all objects in the correct order.\n     */\n    @Test\n    public void iteratorTest() throws IOException {\n        var result = new ArrayList<URL>();\n\n        var it = createIterator(pageURLs.get(0));\n        while (it.hasNext()) {\n            result.add(it.next().getLocation());\n        }\n\n        assertThat(result).isEqualTo(resourceURLs);\n    }\n\n    /**\n     * Test unusual {@link Iterator#next()} and {@link Iterator#hasNext()} usage.\n     */\n    @Test\n    public void nextHasNextTest() throws IOException {\n        var result = new ArrayList<URL>();\n\n        var it = createIterator(pageURLs.get(0));\n        assertThat(it.hasNext()).isTrue();\n        assertThat(it.hasNext()).isTrue();\n\n        // don't try this at home, kids...\n        try {\n            for (;;) {\n                result.add(it.next().getLocation());\n            }\n        } catch (NoSuchElementException ex) {\n            assertThat(it.hasNext()).isFalse();\n            assertThat(it.hasNext()).isFalse();\n        }\n\n        assertThat(result).isEqualTo(resourceURLs);\n    }\n\n    /**\n     * Test that {@link Iterator#remove()} fails.\n     */\n    @Test\n    public void removeTest() {\n        assertThrows(UnsupportedOperationException.class, () -> {\n            var it = createIterator(pageURLs.get(0));\n            it.next();\n            it.remove(); // throws UnsupportedOperationException\n        });\n    }\n\n    /**\n     * Creates a new {@link Iterator} of {@link Authorization} objects.\n     *\n     * @param first\n     *            URL of the first page\n     * @return Created {@link Iterator}\n     */\n    private Iterator<Authorization> createIterator(URL first) throws IOException {\n        var provider = new TestableConnectionProvider() {\n            private int ix;\n\n            @Override\n            public int sendSignedPostAsGetRequest(URL url, Login login) {\n                ix = pageURLs.indexOf(url);\n                assertThat(ix).isGreaterThanOrEqualTo(0);\n                return HttpURLConnection.HTTP_OK;\n            }\n\n            @Override\n            public JSON readJsonResponse() {\n                var start = ix * RESOURCES_PER_PAGE;\n                var end = (ix + 1) * RESOURCES_PER_PAGE;\n\n                var cb = new JSONBuilder();\n                cb.array(TYPE, resourceURLs.subList(start, end));\n\n                return JSON.parse(cb.toString());\n            }\n\n            @Override\n            public Collection<URL> getLinks(String relation) {\n                if (\"next\".equals(relation) && (ix + 1 < pageURLs.size())) {\n                    return Collections.singletonList(pageURLs.get(ix + 1));\n                }\n                return Collections.emptyList();\n            }\n        };\n\n        var login = provider.createLogin();\n\n        provider.close();\n\n        return new ResourceIterator<>(login, TYPE, first, Login::bindAuthorization);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.assertj.core.api.SoftAssertions;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit test for {@link Resource}.\n */\npublic class ResourceTest {\n\n    /**\n     * Test {@link Resource#path()}.\n     */\n    @Test\n    public void testPath() {\n        SoftAssertions.assertSoftly(softly -> {\n            softly.assertThat(Resource.NEW_NONCE.path()).isEqualTo(\"newNonce\");\n            softly.assertThat(Resource.NEW_ACCOUNT.path()).isEqualTo(\"newAccount\");\n            softly.assertThat(Resource.NEW_ORDER.path()).isEqualTo(\"newOrder\");\n            softly.assertThat(Resource.NEW_AUTHZ.path()).isEqualTo(\"newAuthz\");\n            softly.assertThat(Resource.REVOKE_CERT.path()).isEqualTo(\"revokeCert\");\n            softly.assertThat(Resource.KEY_CHANGE.path()).isEqualTo(\"keyChange\");\n            softly.assertThat(Resource.RENEWAL_INFO.path()).isEqualTo(\"renewalInfo\");\n        });\n\n        // fails if there are untested future Resource values\n        assertThat(Resource.values()).hasSize(7);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;\n\nimport java.net.URI;\nimport java.net.URL;\nimport java.net.http.HttpClient;\nimport java.util.ServiceLoader;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.challenge.Challenge;\nimport org.shredzone.acme4j.provider.AcmeProvider;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * Unit tests for {@link Session#provider()}. Requires that both enclosed\n * {@link AcmeProvider} implementations are registered via Java's {@link ServiceLoader}\n * API when the test is run.\n */\npublic class SessionProviderTest {\n\n    /**\n     * There are no testing providers accepting {@code acme://example.org}. Test that\n     * connecting to this URI will result in an {@link IllegalArgumentException}.\n     */\n    @Test\n    public void testNone() {\n        assertThatIllegalArgumentException()\n                .isThrownBy(() -> new Session(new URI(\"acme://example.org\")).provider())\n                .withMessage(\"No ACME provider found for acme://example.org\");\n    }\n\n    /**\n     * Test that connecting to an acme URI will return an {@link AcmeProvider}, and that\n     * the result is cached.\n     */\n    @Test\n    public void testConnectURI() throws Exception {\n        var session = new Session(new URI(\"acme://example.com\"));\n\n        var provider = session.provider();\n        assertThat(provider).isInstanceOf(Provider1.class);\n\n        var provider2 = session.provider();\n        assertThat(provider2).isInstanceOf(Provider1.class);\n        assertThat(provider2).isSameAs(provider);\n    }\n\n    /**\n     * There are two testing providers accepting {@code acme://example.net}. Test that\n     * connecting to this URI will result in an {@link IllegalArgumentException}.\n     */\n    @Test\n    public void testDuplicate() {\n        assertThatIllegalArgumentException()\n                .isThrownBy(() -> new Session(new URI(\"acme://example.net\")).provider())\n                .withMessage(\"Both ACME providers Provider1 and Provider2 accept\" +\n                        \" acme://example.net. Please check your classpath.\");\n    }\n\n    public static class Provider1 implements AcmeProvider {\n        @Override\n        public boolean accepts(URI serverUri) {\n            return \"acme\".equals(serverUri.getScheme())\n                    && (\"example.com\".equals(serverUri.getHost())\n                           || \"example.net\".equals(serverUri.getHost()));\n        }\n\n        @Override\n        public Connection connect(URI serverUri, NetworkSettings networkSettings, HttpClient httpClient) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public URL resolve(URI serverUri) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public JSON directory(Session session, URI serverUri) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public Challenge createChallenge(Login login, JSON data) {\n            throw new UnsupportedOperationException();\n        }\n    }\n\n    public static class Provider2 implements AcmeProvider {\n        @Override\n        public boolean accepts(URI serverUri) {\n            return \"acme\".equals(serverUri.getScheme())\n                    && \"example.net\".equals(serverUri.getHost());\n        }\n\n        @Override\n        public Connection connect(URI serverUri, NetworkSettings networkSettings, HttpClient httpClient) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public URL resolve(URI serverUri) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public JSON directory(Session session, URI serverUri) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public Challenge createChallenge(Login login, JSON data) {\n            throw new UnsupportedOperationException();\n        }\n    }\n\n}"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/connector/TrimmingInputStreamTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.connector;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link TrimmingInputStream}.\n */\npublic class TrimmingInputStreamTest {\n    private final static String FULL_TEXT =\n              \"Gallia est omnis divisa in partes tres,\\r\\n\\r\\n\\r\\n\"\n            + \"quarum unam incolunt Belgae, aliam Aquitani,\\r\\r\\r\\n\\n\"\n            + \"tertiam, qui ipsorum lingua Celtae, nostra Galli appellantur.\";\n\n    private final static String TRIMMED_TEXT =\n              \"Gallia est omnis divisa in partes tres,\\n\"\n            + \"quarum unam incolunt Belgae, aliam Aquitani,\\n\"\n            + \"tertiam, qui ipsorum lingua Celtae, nostra Galli appellantur.\";\n\n    @Test\n    public void testEmpty() throws IOException {\n        var out = trimByStream(\"\");\n        assertThat(out).isEqualTo(\"\");\n    }\n\n    @Test\n    public void testLineBreakOnly() throws IOException {\n        var out1 = trimByStream(\"\\n\");\n        assertThat(out1).isEqualTo(\"\");\n\n        var out2 = trimByStream(\"\\r\");\n        assertThat(out2).isEqualTo(\"\");\n\n        var out3 = trimByStream(\"\\r\\n\");\n        assertThat(out3).isEqualTo(\"\");\n    }\n\n    @Test\n    public void testTrim() throws IOException {\n        var out = trimByStream(FULL_TEXT);\n        assertThat(out).isEqualTo(TRIMMED_TEXT);\n    }\n\n    @Test\n    public void testTrimEndOnly() throws IOException {\n        var out = trimByStream(FULL_TEXT + \"\\r\\n\\r\\n\");\n        assertThat(out).isEqualTo(TRIMMED_TEXT + \"\\n\");\n    }\n\n    @Test\n    public void testTrimStartOnly() throws IOException {\n        var out = trimByStream(\"\\n\\n\" + FULL_TEXT);\n        assertThat(out).isEqualTo(TRIMMED_TEXT);\n    }\n\n    @Test\n    public void testTrimFull() throws IOException {\n        var out = trimByStream(\"\\n\\n\" + FULL_TEXT + \"\\r\\n\\r\\n\");\n        assertThat(out).isEqualTo(TRIMMED_TEXT + \"\\n\");\n    }\n\n    @Test\n    public void testAvailable() throws IOException {\n        try (var in = new TrimmingInputStream(\n                new ByteArrayInputStream(\"Test\".getBytes(StandardCharsets.US_ASCII)))) {\n            assertThat(in.available()).isNotEqualTo(0);\n        }\n    }\n\n    /**\n     * Trims a string by running it through the {@link TrimmingInputStream}.\n     */\n    private String trimByStream(String str) throws IOException {\n        var out = new StringBuilder();\n\n        try (var in = new TrimmingInputStream(\n                        new ByteArrayInputStream(str.getBytes(StandardCharsets.US_ASCII)))) {\n            int ch;\n            while ((ch = in.read()) >= 0) {\n                out.append((char) ch);\n            }\n        }\n\n        return out.toString();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeExceptionTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link AcmeException}.\n */\npublic class AcmeExceptionTest {\n\n    @Test\n    public void testAcmeException() {\n        var ex = new AcmeException();\n        assertThat(ex.getMessage()).isNull();\n        assertThat(ex.getCause()).isNull();\n    }\n\n    @Test\n    public void testMessageAcmeException() {\n        var message = \"Failure\";\n        var ex = new AcmeException(message);\n        assertThat(ex.getMessage()).isEqualTo(message);\n        assertThat(ex.getCause()).isNull();\n    }\n\n    @Test\n    public void testCausedAcmeException() {\n        var message = \"Failure\";\n        var cause = new IOException(\"No network\");\n\n        var ex = new AcmeException(message, cause);\n        assertThat(ex.getMessage()).isEqualTo(message);\n        assertThat(ex.getCause()).isEqualTo(cause);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeLazyLoadingExceptionTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\n\nimport java.io.Serial;\nimport java.net.URL;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.AcmeResource;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link AcmeLazyLoadingException}.\n */\npublic class AcmeLazyLoadingExceptionTest {\n\n    private final URL resourceUrl = TestUtils.url(\"http://example.com/acme/resource/123\");\n\n    @Test\n    public void testAcmeLazyLoadingException() {\n        var login = mock(Login.class);\n        var resource = new TestResource(login, resourceUrl);\n\n        var cause = new AcmeException(\"Something went wrong\");\n\n        var ex = new AcmeLazyLoadingException(resource, cause);\n        assertThat(ex).isInstanceOf(RuntimeException.class);\n        assertThat(ex.getMessage()).contains(resourceUrl.toString());\n        assertThat(ex.getMessage()).contains(TestResource.class.getSimpleName());\n        assertThat(ex.getCause()).isEqualTo(cause);\n        assertThat(ex.getType()).isEqualTo(TestResource.class);\n        assertThat(ex.getLocation()).isEqualTo(resourceUrl);\n    }\n\n    private static class TestResource extends AcmeResource {\n        @Serial\n        private static final long serialVersionUID = 1023419539450677538L;\n\n        public TestResource(Login login, URL location) {\n            super(login, location);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeNetworkExceptionTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link AcmeNetworkException}.\n */\npublic class AcmeNetworkExceptionTest {\n\n    @Test\n    public void testAcmeNetworkException() {\n        var cause = new IOException(\"Network not reachable\");\n\n        var ex = new AcmeNetworkException(cause);\n\n        assertThat(ex.getMessage()).isNotNull();\n        assertThat(ex.getCause()).isEqualTo(cause);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeNotSupportedExceptionTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2023 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link AcmeNotSupportedException}.\n */\npublic class AcmeNotSupportedExceptionTest {\n\n    @Test\n    public void testAcmeNotSupportedException() {\n        var msg = \"revoke\";\n        var ex = new AcmeNotSupportedException(msg);\n        assertThat(ex).isInstanceOf(RuntimeException.class);\n        assertThat(ex.getMessage()).isEqualTo(\"Server does not support revoke\");\n        assertThat(ex.getCause()).isNull();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeProtocolExceptionTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link AcmeProtocolException}.\n */\npublic class AcmeProtocolExceptionTest {\n\n    @Test\n    public void testAcmeProtocolException() {\n        var msg = \"Bad content\";\n        var ex = new AcmeProtocolException(msg);\n        assertThat(ex).isInstanceOf(RuntimeException.class);\n        assertThat(ex.getMessage()).isEqualTo(msg);\n        assertThat(ex.getCause()).isNull();\n    }\n\n    @Test\n    public void testCausedAcmeProtocolException() {\n        var message = \"Bad content\";\n        var cause = new NumberFormatException(\"Not a number: abc\");\n        var ex = new AcmeProtocolException(message, cause);\n        assertThat(ex.getMessage()).isEqualTo(message);\n        assertThat(ex.getCause()).isEqualTo(cause);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeRateLimitedExceptionTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.shredzone.acme4j.toolbox.TestUtils.createProblem;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.net.URI;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Arrays;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link AcmeRateLimitedException}.\n */\npublic class AcmeRateLimitedExceptionTest {\n\n    /**\n     * Test that parameters are correctly returned.\n     */\n    @Test\n    public void testAcmeRateLimitedException() {\n        var type = URI.create(\"urn:ietf:params:acme:error:rateLimited\");\n        var detail = \"Too many requests per minute\";\n        var retryAfter = Instant.now().plus(Duration.ofMinutes(1));\n        var documents = Arrays.asList(\n                        url(\"http://example.com/doc1.html\"),\n                        url(\"http://example.com/doc2.html\"));\n\n        var problem = createProblem(type, detail, null);\n\n        var ex = new AcmeRateLimitedException(problem, retryAfter, documents);\n\n        assertThat(ex.getType()).isEqualTo(type);\n        assertThat(ex.getMessage()).isEqualTo(detail);\n        assertThat(ex.getRetryAfter().orElseThrow()).isEqualTo(retryAfter);\n        assertThat(ex.getDocuments()).containsAll(documents);\n    }\n\n    /**\n     * Test that optional parameters are null-safe.\n     */\n    @Test\n    public void testNullAcmeRateLimitedException() {\n        var type = URI.create(\"urn:ietf:params:acme:error:rateLimited\");\n        var detail = \"Too many requests per minute\";\n\n        var problem = createProblem(type, detail, null);\n\n        var ex = new AcmeRateLimitedException(problem, null, null);\n\n        assertThat(ex.getType()).isEqualTo(type);\n        assertThat(ex.getMessage()).isEqualTo(detail);\n        assertThat(ex.getRetryAfter()).isEmpty();\n        assertThat(ex.getDocuments()).isEmpty();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeUserActionRequiredExceptionTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.exception;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.shredzone.acme4j.toolbox.TestUtils.createProblem;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link AcmeUserActionRequiredException}.\n */\npublic class AcmeUserActionRequiredExceptionTest {\n\n    /**\n     * Test that parameters are correctly returned.\n     */\n    @Test\n    public void testAcmeUserActionRequiredException() throws MalformedURLException {\n        var type = URI.create(\"urn:ietf:params:acme:error:userActionRequired\");\n        var detail = \"Accept new TOS\";\n        var tosUri = URI.create(\"http://example.com/agreement.pdf\");\n        var instanceUrl = URI.create(\"http://example.com/howToAgree.html\").toURL();\n\n        var problem = createProblem(type, detail, instanceUrl);\n\n        var ex = new AcmeUserActionRequiredException(problem, tosUri);\n\n        assertThat(ex.getType()).isEqualTo(type);\n        assertThat(ex.getMessage()).isEqualTo(detail);\n        assertThat(ex.getTermsOfServiceUri().orElseThrow()).isEqualTo(tosUri);\n        assertThat(ex.getInstance()).isEqualTo(instanceUrl);\n        assertThat(ex.toString()).isEqualTo(\"Please visit \" + instanceUrl + \" - details: \" + detail);\n    }\n\n    /**\n     * Test that optional parameters are null-safe.\n     */\n    @Test\n    public void testNullAcmeUserActionRequiredException() throws MalformedURLException {\n        var type = URI.create(\"urn:ietf:params:acme:error:userActionRequired\");\n        var detail = \"Call our service\";\n        var instanceUrl = URI.create(\"http://example.com/howToContactUs.html\").toURL();\n\n        var problem = createProblem(type, detail, instanceUrl);\n\n        var ex = new AcmeUserActionRequiredException(problem, null);\n\n        assertThat(ex.getType()).isEqualTo(type);\n        assertThat(ex.getMessage()).isEqualTo(detail);\n        assertThat(ex.getTermsOfServiceUri()).isEmpty();\n        assertThat(ex.getInstance()).isEqualTo(instanceUrl);\n        assertThat(ex.toString()).isEqualTo(\"Please visit \" + instanceUrl + \" - details: \" + detail);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/provider/AbstractAcmeProviderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider;\n\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.Mockito.*;\nimport static org.shredzone.acme4j.toolbox.TestUtils.getJSON;\n\nimport java.net.HttpURLConnection;\nimport java.net.URI;\nimport java.net.URL;\nimport java.net.http.HttpClient;\nimport java.time.ZonedDateTime;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Optional;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.challenge.Challenge;\nimport org.shredzone.acme4j.challenge.Dns01Challenge;\nimport org.shredzone.acme4j.challenge.DnsAccount01Challenge;\nimport org.shredzone.acme4j.challenge.DnsPersist01Challenge;\nimport org.shredzone.acme4j.challenge.Http01Challenge;\nimport org.shredzone.acme4j.challenge.TlsAlpn01Challenge;\nimport org.shredzone.acme4j.challenge.TokenChallenge;\nimport org.shredzone.acme4j.connector.Connection;\nimport org.shredzone.acme4j.connector.DefaultConnection;\nimport org.shredzone.acme4j.connector.HttpConnector;\nimport org.shredzone.acme4j.connector.NetworkSettings;\nimport org.shredzone.acme4j.connector.NonceHolder;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Unit tests for {@link AbstractAcmeProvider}.\n */\npublic class AbstractAcmeProviderTest {\n\n    private static final URI SERVER_URI = URI.create(\"http://example.com/acme\");\n    private static final URL RESOLVED_URL = TestUtils.url(\"http://example.com/acme/directory\");\n    private static final NetworkSettings NETWORK_SETTINGS = new NetworkSettings();\n\n    /**\n     * Test that connect returns a connection.\n     */\n    @Test\n    public void testConnect() {\n        var invoked = new AtomicBoolean();\n        var httpClient = HttpClient.newBuilder().build();\n\n        var provider = new TestAbstractAcmeProvider() {\n            @Override\n            protected HttpConnector createHttpConnector(NetworkSettings settings, HttpClient client) {\n                assertThat(settings).isSameAs(NETWORK_SETTINGS);\n                assertThat(client).isSameAs(httpClient);\n                invoked.set(true);\n                return super.createHttpConnector(settings, client);\n            }\n        };\n\n        var connection = provider.connect(SERVER_URI, NETWORK_SETTINGS, httpClient);\n        assertThat(connection).isNotNull();\n        assertThat(connection).isInstanceOf(DefaultConnection.class);\n        assertThat(invoked).isTrue();\n    }\n\n    /**\n     * Verify that the resources directory is read.\n     */\n    @Test\n    public void testResources() throws AcmeException {\n        var connection = mock(Connection.class);\n        var session = mock(Session.class);\n\n        when(connection.readJsonResponse()).thenReturn(getJSON(\"directory\"));\n        when(session.lockNonce()).thenReturn(mock(NonceHolder.class));\n\n        var provider = new TestAbstractAcmeProvider(connection);\n        var map = provider.directory(session, SERVER_URI);\n\n        assertThatJson(map.toString()).isEqualTo(TestUtils.getJSON(\"directory\").toString());\n\n        verify(connection).sendRequest(RESOLVED_URL, session, null);\n        verify(connection).getNonce();\n        verify(connection).getLastModified();\n        verify(connection).getExpiration();\n        verify(connection).readJsonResponse();\n        verify(connection).close();\n        verifyNoMoreInteractions(connection);\n    }\n\n    /**\n     * Verify that the cache control headers are evaluated.\n     */\n    @Test\n    public void testResourcesCacheControl() throws AcmeException {\n        var lastModified = ZonedDateTime.now().minus(13, ChronoUnit.DAYS);\n        var expiryDate = ZonedDateTime.now().plus(60, ChronoUnit.DAYS);\n\n        var connection = mock(Connection.class);\n        var session = mock(Session.class);\n\n        when(connection.readJsonResponse()).thenReturn(getJSON(\"directory\"));\n        when(connection.getLastModified()).thenReturn(Optional.of(lastModified));\n        when(connection.getExpiration()).thenReturn(Optional.of(expiryDate));\n        when(session.lockNonce()).thenReturn(mock(NonceHolder.class));\n        when(session.getDirectoryExpires()).thenReturn(null);\n        when(session.getDirectoryLastModified()).thenReturn(null);\n        when(session.networkSettings()).thenReturn(NETWORK_SETTINGS);\n        when(session.getHttpClient()).thenReturn(HttpClient.newBuilder().build());\n\n        var provider = new TestAbstractAcmeProvider(connection);\n        var map = provider.directory(session, SERVER_URI);\n\n        assertThatJson(map.toString()).isEqualTo(TestUtils.getJSON(\"directory\").toString());\n\n        verify(session).setDirectoryLastModified(eq(lastModified));\n        verify(session).setDirectoryExpires(eq(expiryDate));\n        verify(session).getDirectoryExpires();\n        verify(session).getDirectoryLastModified();\n        verify(session).networkSettings();\n        verify(session).getHttpClient();\n        verify(session).lockNonce();\n        verifyNoMoreInteractions(session);\n\n        verify(connection).sendRequest(RESOLVED_URL, session, null);\n        verify(connection).getNonce();\n        verify(connection).getLastModified();\n        verify(connection).getExpiration();\n        verify(connection).readJsonResponse();\n        verify(connection).close();\n        verifyNoMoreInteractions(connection);\n    }\n\n    /**\n     * Verify that resorces are not fetched if not yet expired.\n     */\n    @Test\n    public void testResourcesNotExprired() throws AcmeException {\n        var expiryDate = ZonedDateTime.now().plus(60, ChronoUnit.DAYS);\n\n        var connection = mock(Connection.class);\n        var session = mock(Session.class);\n\n        when(session.getDirectoryExpires()).thenReturn(expiryDate);\n\n        var provider = new TestAbstractAcmeProvider();\n        var map = provider.directory(session, SERVER_URI);\n\n        assertThat(map).isNull();\n\n        verify(session).getDirectoryExpires();\n        verifyNoMoreInteractions(session);\n\n        verifyNoMoreInteractions(connection);\n    }\n\n    /**\n     * Verify that resorces are fetched if expired.\n     */\n    @Test\n    public void testResourcesExprired() throws AcmeException {\n        var expiryDate = ZonedDateTime.now().plus(60, ChronoUnit.DAYS);\n        var pastExpiryDate = ZonedDateTime.now().minus(10, ChronoUnit.MINUTES);\n\n        var connection = mock(Connection.class);\n        var session = mock(Session.class);\n\n        when(connection.readJsonResponse()).thenReturn(getJSON(\"directory\"));\n        when(connection.getExpiration()).thenReturn(Optional.of(expiryDate));\n        when(connection.getLastModified()).thenReturn(Optional.empty());\n        when(session.getDirectoryExpires()).thenReturn(pastExpiryDate);\n        when(session.networkSettings()).thenReturn(NETWORK_SETTINGS);\n        when(session.getHttpClient()).thenReturn(HttpClient.newBuilder().build());\n        when(session.lockNonce()).thenReturn(mock(NonceHolder.class));\n\n        var provider = new TestAbstractAcmeProvider(connection);\n        var map = provider.directory(session, SERVER_URI);\n\n        assertThatJson(map.toString()).isEqualTo(TestUtils.getJSON(\"directory\").toString());\n\n        verify(session).setDirectoryExpires(eq(expiryDate));\n        verify(session).setDirectoryLastModified(eq(null));\n        verify(session).getDirectoryExpires();\n        verify(session).getDirectoryLastModified();\n        verify(session).networkSettings();\n        verify(session).getHttpClient();\n        verify(session).lockNonce();\n        verifyNoMoreInteractions(session);\n\n        verify(connection).sendRequest(RESOLVED_URL, session, null);\n        verify(connection).getNonce();\n        verify(connection).getLastModified();\n        verify(connection).getExpiration();\n        verify(connection).readJsonResponse();\n        verify(connection).close();\n        verifyNoMoreInteractions(connection);\n    }\n\n    /**\n     * Verify that if-modified-since is used.\n     */\n    @Test\n    public void testResourcesIfModifiedSince() throws AcmeException {\n        var modifiedSinceDate = ZonedDateTime.now().minus(60, ChronoUnit.DAYS);\n\n        var connection = mock(Connection.class);\n        var session = mock(Session.class);\n\n        when(connection.sendRequest(eq(RESOLVED_URL), eq(session), eq(modifiedSinceDate)))\n                .thenReturn(HttpURLConnection.HTTP_NOT_MODIFIED);\n        when(connection.getLastModified()).thenReturn(Optional.of(modifiedSinceDate));\n        when(session.getDirectoryLastModified()).thenReturn(modifiedSinceDate);\n        when(session.networkSettings()).thenReturn(NETWORK_SETTINGS);\n        when(session.getHttpClient()).thenReturn(HttpClient.newBuilder().build());\n        when(session.lockNonce()).thenReturn(mock(NonceHolder.class));\n\n        var provider = new TestAbstractAcmeProvider(connection);\n        var map = provider.directory(session, SERVER_URI);\n\n        assertThat(map).isNull();\n\n        verify(session).getDirectoryExpires();\n        verify(session).getDirectoryLastModified();\n        verify(session).networkSettings();\n        verify(session).getHttpClient();\n        verify(session).lockNonce();\n        verifyNoMoreInteractions(session);\n\n        verify(connection).sendRequest(RESOLVED_URL, session, modifiedSinceDate);\n        verify(connection).close();\n        verifyNoMoreInteractions(connection);\n    }\n\n    /**\n     * Test that challenges are generated properly.\n     */\n    @Test\n    public void testCreateChallenge() {\n        var login = TestUtils.login();\n\n        var provider = new TestAbstractAcmeProvider();\n\n        var c1 = provider.createChallenge(login, getJSON(\"httpChallenge\"));\n        assertThat(c1).isNotNull();\n        assertThat(c1).isInstanceOf(Http01Challenge.class);\n\n        var c2 = provider.createChallenge(login, getJSON(\"httpChallenge\"));\n        assertThat(c2).isNotSameAs(c1);\n\n        var c3 = provider.createChallenge(login, getJSON(\"dns01Challenge\"));\n        assertThat(c3).isNotNull();\n        assertThat(c3).isInstanceOf(Dns01Challenge.class);\n\n        var c4 = provider.createChallenge(login, getJSON(\"dnsAccount01Challenge\"));\n        assertThat(c4).isNotNull();\n        assertThat(c4).isInstanceOf(DnsAccount01Challenge.class);\n\n        var c8 = provider.createChallenge(login, getJSON(\"dnsPersist01Challenge\"));\n        assertThat(c8).isNotNull();\n        assertThat(c8).isInstanceOf(DnsPersist01Challenge.class);\n\n        var c5 = provider.createChallenge(login, getJSON(\"tlsAlpnChallenge\"));\n        assertThat(c5).isNotNull();\n        assertThat(c5).isInstanceOf(TlsAlpn01Challenge.class);\n\n        var json6 = new JSONBuilder()\n                    .put(\"type\", \"foobar-01\")\n                    .put(\"url\", \"https://example.com/some/challenge\")\n                    .toJSON();\n        var c6 = provider.createChallenge(login, json6);\n        assertThat(c6).isNotNull();\n        assertThat(c6).isInstanceOf(Challenge.class);\n\n        var json7 = new JSONBuilder()\n                        .put(\"type\", \"foobar-01\")\n                        .put(\"token\", \"abc123\")\n                        .put(\"url\", \"https://example.com/some/challenge\")\n                        .toJSON();\n        var c7 = provider.createChallenge(login, json7);\n        assertThat(c7).isNotNull();\n        assertThat(c7).isInstanceOf(TokenChallenge.class);\n\n        assertThrows(AcmeProtocolException.class, () -> {\n            var json8 = new JSONBuilder()\n                    .put(\"url\", \"https://example.com/some/challenge\")\n                    .toJSON();\n            provider.createChallenge(login, json8);\n        });\n\n        assertThrows(NullPointerException.class, () -> provider.createChallenge(login, null));\n    }\n\n    private static class TestAbstractAcmeProvider extends AbstractAcmeProvider {\n        private final Connection connection;\n\n        public TestAbstractAcmeProvider() {\n            this.connection = null;\n        }\n\n        public TestAbstractAcmeProvider(Connection connection) {\n            this.connection = connection;\n        }\n\n        @Override\n        public boolean accepts(URI serverUri) {\n            assertThat(serverUri).isEqualTo(SERVER_URI);\n            return true;\n        }\n\n        @Override\n        public URL resolve(URI serverUri) {\n            assertThat(serverUri).isEqualTo(SERVER_URI);\n            return RESOLVED_URL;\n        }\n\n        @Override\n        public Connection connect(URI serverUri, NetworkSettings networkSettings, HttpClient httpClient) {\n            assertThat(serverUri).isEqualTo(SERVER_URI);\n            return connection != null ? connection : super.connect(serverUri, networkSettings, httpClient);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/provider/GenericAcmeProviderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.shredzone.acme4j.toolbox.TestUtils.DEFAULT_NETWORK_SETTINGS;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.http.HttpClient;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.connector.DefaultConnection;\n\n/**\n * Unit tests for {@link GenericAcmeProvider}.\n */\npublic class GenericAcmeProviderTest {\n\n    /**\n     * Tests if the provider accepts the correct URIs.\n     */\n    @Test\n    public void testAccepts() throws URISyntaxException {\n        var provider = new GenericAcmeProvider();\n\n        assertThat(provider.accepts(new URI(\"http://example.com/acme\"))).isTrue();\n        assertThat(provider.accepts(new URI(\"https://example.com/acme\"))).isTrue();\n        assertThat(provider.accepts(new URI(\"acme://example.com\"))).isFalse();\n    }\n\n    /**\n     * Test if the provider resolves the URI correctly.\n     */\n    @Test\n    public void testResolve() throws URISyntaxException {\n        var serverUri = new URI(\"http://example.com/acme?foo=abc&bar=123\");\n\n        var provider = new GenericAcmeProvider();\n\n        var resolvedUrl = provider.resolve(serverUri);\n        assertThat(resolvedUrl.toString()).isEqualTo(serverUri.toString());\n\n        var httpClient = HttpClient.newBuilder().build();\n        var connection = provider.connect(serverUri, DEFAULT_NETWORK_SETTINGS, httpClient);\n        assertThat(connection).isInstanceOf(DefaultConnection.class);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/provider/TestableConnectionProvider.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.net.http.HttpClient;\nimport java.security.KeyPair;\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.BiFunction;\n\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.challenge.Challenge;\nimport org.shredzone.acme4j.connector.Connection;\nimport org.shredzone.acme4j.connector.DummyConnection;\nimport org.shredzone.acme4j.connector.NetworkSettings;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\nimport org.shredzone.acme4j.toolbox.TestUtils;\n\n/**\n * Test implementation of {@link AcmeProvider}. It also implements a dummy implementation\n * of {@link Connection} that is always returned on {@link #connect(URI, NetworkSettings)}.\n */\npublic class TestableConnectionProvider extends DummyConnection implements AcmeProvider {\n    private final Map<String, BiFunction<Login, JSON, Challenge>> creatorMap = new HashMap<>();\n    private final Map<String, Challenge> createdMap = new HashMap<>();\n    private final JSONBuilder directory = new JSONBuilder();\n    private final KeyPair keyPair = TestUtils.createKeyPair();\n    private JSONBuilder metadata = null;\n\n    /**\n     * Register a {@link Resource} mapping.\n     *\n     * @param r\n     *            {@link Resource} to be mapped\n     * @param u\n     *            {@link URL} to be returned\n     */\n    public void putTestResource(Resource r, URL u) {\n        directory.put(r.path(), u);\n    }\n\n    /**\n     * Add a property to the metadata registry.\n     *\n     * @param key\n     *            Metadata key\n     * @param value\n     *            Metadata value\n     */\n    public void putMetadata(String key, Object value) {\n        if (metadata == null) {\n            metadata = directory.object(\"meta\");\n        }\n        metadata.put(key, value);\n    }\n\n    /**\n     * Register a {@link Challenge}.\n     *\n     * @param type\n     *            Challenge type to register.\n     * @param creator\n     *            Creator {@link BiFunction} that creates a matching {@link Challenge}\n     */\n    public void putTestChallenge(String type, BiFunction<Login, JSON, Challenge> creator) {\n        creatorMap.put(type, creator);\n    }\n\n    /**\n     * Returns the {@link Challenge} instance that has been created. Fails if no such\n     * challenge was created.\n     *\n     * @param type Challenge type\n     * @return Created {@link Challenge} instance\n     */\n    public Challenge getChallenge(String type) {\n        if (!createdMap.containsKey(type)) {\n            throw new IllegalArgumentException(\"No challenge of type \" + type + \" was created\");\n        }\n        return createdMap.get(type);\n    }\n\n    /**\n     * Creates a {@link Session} that uses this {@link AcmeProvider}.\n     */\n    public Session createSession() {\n        return TestUtils.session(this);\n    }\n\n    /**\n     * Creates a {@link Login} that uses this {@link AcmeProvider}.\n     */\n    public Login createLogin() throws IOException {\n        var session = createSession();\n        return session.login(URI.create(TestUtils.ACCOUNT_URL).toURL(), keyPair);\n    }\n\n    public KeyPair getAccountKeyPair() {\n        return keyPair;\n    }\n\n    @Override\n    public Optional<Instant> getRetryAfter() {\n        return Optional.empty();\n    }\n\n    @Override\n    public boolean accepts(URI serverUri) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public URL resolve(URI serverUri) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Connection connect(URI serverUri, NetworkSettings networkSettings, HttpClient httpClient) {\n        return this;\n    }\n\n    @Override\n    public JSON directory(Session session, URI serverUri) {\n        if (directory.toMap().isEmpty()) {\n            throw new UnsupportedOperationException();\n        }\n        return directory.toJSON();\n    }\n\n    @Override\n    public Challenge createChallenge(Login login, JSON data) {\n        if (creatorMap.isEmpty()) {\n            throw new UnsupportedOperationException();\n        }\n\n        Challenge created;\n\n        var type = data.get(\"type\").asString();\n        if (creatorMap.containsKey(type)) {\n            created = creatorMap.get(type).apply(login, data);\n        } else {\n            created = new Challenge(login, data);\n        }\n\n        createdMap.put(type, created);\n\n        return created;\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/provider/actalis/ActalisAcmeProviderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2025 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider.actalis;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\n\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.junit.jupiter.api.Test;\n\npublic class ActalisAcmeProviderTest {\n\n    private static final String PRODUCTION_DIRECTORY_URL = \"https://acme-api.actalis.com/acme/directory\";\n\n    /**\n     * Tests if the provider accepts the correct URIs.\n     */\n    @Test\n    public void testAccepts() throws URISyntaxException {\n        var provider = new ActalisAcmeProvider();\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(provider.accepts(new URI(\"acme://actalis.com\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://actalis.com/\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://example.com\"))).isFalse();\n            softly.assertThat(provider.accepts(new URI(\"http://example.com/acme\"))).isFalse();\n            softly.assertThat(provider.accepts(new URI(\"https://example.com/acme\"))).isFalse();\n        }\n    }\n\n    /**\n     * Test if acme URIs are properly resolved.\n     */\n    @Test\n    public void testResolve() throws URISyntaxException {\n        var provider = new ActalisAcmeProvider();\n\n        assertThat(provider.resolve(new URI(\"acme://actalis.com\"))).isEqualTo(url(PRODUCTION_DIRECTORY_URL));\n        assertThat(provider.resolve(new URI(\"acme://actalis.com/\"))).isEqualTo(url(PRODUCTION_DIRECTORY_URL));\n\n        assertThrows(IllegalArgumentException.class, () -> provider.resolve(new URI(\"acme://letsencrypt.org/v99\")));\n    }\n\n}"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/provider/google/GoogleAcmeProviderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2024 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider.google;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\n\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link GoogleAcmeProvider}.\n */\npublic class GoogleAcmeProviderTest {\n\n    private static final String PRODUCTION_DIRECTORY_URL = \"https://dv.acme-v02.api.pki.goog/directory\";\n    private static final String STAGING_DIRECTORY_URL = \"https://dv.acme-v02.test-api.pki.goog/directory\";\n\n    /**\n     * Tests if the provider accepts the correct URIs.\n     */\n    @Test\n    public void testAccepts() throws URISyntaxException {\n        var provider = new GoogleAcmeProvider();\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(provider.accepts(new URI(\"acme://pki.goog\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://pki.goog/\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://pki.goog/staging\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://example.com\"))).isFalse();\n            softly.assertThat(provider.accepts(new URI(\"http://example.com/acme\"))).isFalse();\n            softly.assertThat(provider.accepts(new URI(\"https://example.com/acme\"))).isFalse();\n        }\n    }\n\n    /**\n     * Test if acme URIs are properly resolved.\n     */\n    @Test\n    public void testResolve() throws URISyntaxException {\n        var provider = new GoogleAcmeProvider();\n\n        assertThat(provider.resolve(new URI(\"acme://pki.goog\"))).isEqualTo(url(PRODUCTION_DIRECTORY_URL));\n        assertThat(provider.resolve(new URI(\"acme://pki.goog/\"))).isEqualTo(url(PRODUCTION_DIRECTORY_URL));\n        assertThat(provider.resolve(new URI(\"acme://pki.goog/staging\"))).isEqualTo(url(STAGING_DIRECTORY_URL));\n\n        assertThrows(IllegalArgumentException.class, () -> provider.resolve(new URI(\"acme://pki.goog/v99\")));\n    }\n\n    /**\n     * Test if correct MAC algorithm is proposed.\n     */\n    @Test\n    public void testMacAlgorithm() {\n        var provider = new GoogleAcmeProvider();\n\n        assertThat(provider.getProposedEabMacAlgorithm()).isNotEmpty().contains(\"HS256\");\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/provider/letsencrypt/LetsEncryptAcmeProviderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider.letsencrypt;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\n\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link LetsEncryptAcmeProvider}.\n */\npublic class LetsEncryptAcmeProviderTest {\n\n    private static final String V02_DIRECTORY_URL = \"https://acme-v02.api.letsencrypt.org/directory\";\n    private static final String STAGING_DIRECTORY_URL = \"https://acme-staging-v02.api.letsencrypt.org/directory\";\n\n    /**\n     * Tests if the provider accepts the correct URIs.\n     */\n    @Test\n    public void testAccepts() throws URISyntaxException {\n        var provider = new LetsEncryptAcmeProvider();\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(provider.accepts(new URI(\"acme://letsencrypt.org\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://letsencrypt.org/\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://letsencrypt.org/staging\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://letsencrypt.org/v02\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://example.com\"))).isFalse();\n            softly.assertThat(provider.accepts(new URI(\"http://example.com/acme\"))).isFalse();\n            softly.assertThat(provider.accepts(new URI(\"https://example.com/acme\"))).isFalse();\n        }\n    }\n\n    /**\n     * Test if acme URIs are properly resolved.\n     */\n    @Test\n    public void testResolve() throws URISyntaxException {\n        var provider = new LetsEncryptAcmeProvider();\n\n        assertThat(provider.resolve(new URI(\"acme://letsencrypt.org\"))).isEqualTo(url(V02_DIRECTORY_URL));\n        assertThat(provider.resolve(new URI(\"acme://letsencrypt.org/\"))).isEqualTo(url(V02_DIRECTORY_URL));\n        assertThat(provider.resolve(new URI(\"acme://letsencrypt.org/v02\"))).isEqualTo(url(V02_DIRECTORY_URL));\n        assertThat(provider.resolve(new URI(\"acme://letsencrypt.org/staging\"))).isEqualTo(url(STAGING_DIRECTORY_URL));\n\n        assertThrows(IllegalArgumentException.class, () -> provider.resolve(new URI(\"acme://letsencrypt.org/v99\")));\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProviderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider.pebble;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.http.HttpClient;\nimport java.security.cert.CertificateFactory;\nimport java.security.cert.X509Certificate;\nimport javax.net.ssl.TrustManagerFactory;\nimport javax.net.ssl.X509TrustManager;\n\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.connector.NetworkSettings;\n\n/**\n * Unit tests for {@link PebbleAcmeProvider}.\n */\npublic class PebbleAcmeProviderTest {\n\n    /**\n     * Tests if the provider accepts the correct URIs.\n     */\n    @Test\n    public void testAccepts() throws URISyntaxException {\n        var provider = new PebbleAcmeProvider();\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(provider.accepts(new URI(\"acme://pebble\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://pebble/\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://pebble:12345\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://pebble:12345/\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://pebble/some-host.example.com\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://pebble/some-host.example.com:12345\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://example.com\"))).isFalse();\n            softly.assertThat(provider.accepts(new URI(\"http://example.com/acme\"))).isFalse();\n            softly.assertThat(provider.accepts(new URI(\"https://example.com/acme\"))).isFalse();\n        }\n    }\n\n    /**\n     * Test if acme URIs are properly resolved.\n     */\n    @Test\n    public void testResolve() throws URISyntaxException {\n        var provider = new PebbleAcmeProvider();\n\n        assertThat(provider.resolve(new URI(\"acme://pebble\")))\n                .isEqualTo(url(\"https://localhost:14000/dir\"));\n        assertThat(provider.resolve(new URI(\"acme://pebble/\")))\n                .isEqualTo(url(\"https://localhost:14000/dir\"));\n        assertThat(provider.resolve(new URI(\"acme://pebble:12345\")))\n            .isEqualTo(url(\"https://localhost:12345/dir\"));\n        assertThat(provider.resolve(new URI(\"acme://pebble:12345/\")))\n            .isEqualTo(url(\"https://localhost:12345/dir\"));\n        assertThat(provider.resolve(new URI(\"acme://pebble/pebble.example.com\")))\n                .isEqualTo(url(\"https://pebble.example.com:14000/dir\"));\n        assertThat(provider.resolve(new URI(\"acme://pebble/pebble.example.com:12345\")))\n                .isEqualTo(url(\"https://pebble.example.com:12345/dir\"));\n        assertThat(provider.resolve(new URI(\"acme://pebble/pebble.example.com:12345/\")))\n                .isEqualTo(url(\"https://pebble.example.com:12345/dir\"));\n\n        assertThrows(IllegalArgumentException.class, () ->\n                provider.resolve(new URI(\"acme://pebble/bad.example.com:port\")));\n\n        assertThrows(IllegalArgumentException.class, () ->\n                provider.resolve(new URI(\"acme://pebble/bad.example.com:1234/foo\")));\n    }\n\n    /**\n     * Test that createPebbleTrustManagerFactory creates a TrustManagerFactory\n     * with the Pebble certificate loaded from the PEM file.\n     */\n    @Test\n    public void testCreatePebbleTrustManagerFactory() throws Exception {\n        var provider = new PebbleAcmeProvider();\n        \n        // Create the TrustManagerFactory\n        TrustManagerFactory tmf = provider.createPebbleTrustManagerFactory();\n        assertThat(tmf).isNotNull();\n        \n        // Get the trust managers\n        javax.net.ssl.TrustManager[] trustManagers = tmf.getTrustManagers();\n        assertThat(trustManagers.length).isGreaterThan(0);\n        \n        // Find an X509TrustManager\n        X509TrustManager x509TrustManager = null;\n        for (javax.net.ssl.TrustManager tm : trustManagers) {\n            if (tm instanceof X509TrustManager) {\n                x509TrustManager = (X509TrustManager) tm;\n                break;\n            }\n        }\n        assertThat(x509TrustManager).isNotNull();\n        \n        // Verify the Pebble certificate is in the accepted issuers\n        X509Certificate[] acceptedIssuers = x509TrustManager.getAcceptedIssuers();\n        assertThat(acceptedIssuers.length).isGreaterThan(0);\n        \n        // Load the Pebble certificate from the resource to compare\n        X509Certificate pebbleCert = loadPebbleCertificate();\n        assertThat(pebbleCert).isNotNull();\n        \n        // Verify that the Pebble certificate is in the accepted issuers\n        boolean foundPebbleCert = false;\n        for (X509Certificate cert : acceptedIssuers) {\n            if (cert.getSerialNumber().equals(pebbleCert.getSerialNumber()) &&\n                cert.getIssuerDN().equals(pebbleCert.getIssuerDN())) {\n                foundPebbleCert = true;\n                break;\n            }\n        }\n        \n        // Verify the Pebble certificate is present in the trust store\n        assertThat(foundPebbleCert)\n                .as(\"Pebble certificate should be present in the TrustManagerFactory\")\n                .isTrue();\n    }\n    \n    /**\n     * Test that createHttpClient creates an HttpClient with Pebble SSL context\n     * and verifies it calls createPebbleTrustManagerFactory when creating the SSL context.\n     */\n    @Test\n    public void testCreateHttpClient() throws Exception {\n        var provider = spy(new PebbleAcmeProvider());\n        var settings = new NetworkSettings();\n\n        var httpClient = provider.createHttpClient(settings);\n\n        assertThat(httpClient).isNotNull();\n        assertThat(httpClient.followRedirects()).isEqualTo(HttpClient.Redirect.NORMAL);\n        assertThat(httpClient.connectTimeout().orElseThrow()).isEqualTo(settings.getTimeout());\n        \n        // Verify that createPebbleTrustManagerFactory was called exactly once\n        // (it's called when creating the SSL context, which happens once per createHttpClient call)\n        verify(provider).createPebbleTrustManagerFactory();\n        \n        // Verify that the SSL context is configured (not null)\n        var sslContext = httpClient.sslContext();\n        assertThat(sslContext).isNotNull();\n        \n        // Verify Pebble-specific SSL context properties\n        // These properties confirm that the SSL context was created using createPebbleTrustManagerFactory\n        assertThat(sslContext.getProtocol()).isEqualTo(\"TLS\");\n        \n        // Verify the SSL context is properly initialized\n        assertThat(sslContext.getProvider()).isNotNull();\n    }\n    \n    /**\n     * Loads the Pebble certificate from the resource file.\n     * This matches how PebbleAcmeProvider loads it.\n     */\n    private X509Certificate loadPebbleCertificate() throws Exception {\n        // Try the same resource paths as PebbleAcmeProvider\n        String[] resourcePaths = {\n            \"/pebble.minica.pem\",\n            \"/META-INF/pebble.minica.pem\",\n            \"/org/shredzone/acme4j/provider/pebble/pebble.minica.pem\"\n        };\n        \n        for (String resourcePath : resourcePaths) {\n            try (InputStream in = PebbleAcmeProvider.class.getResourceAsStream(resourcePath)) {\n                if (in != null) {\n                    CertificateFactory cf = CertificateFactory.getInstance(\"X.509\");\n                    return (X509Certificate) cf.generateCertificate(in);\n                }\n            }\n        }\n        \n        throw new AssertionError(\"Could not find Pebble certificate resource\");\n    }\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/provider/sslcom/SslComAcmeProviderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2024 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider.sslcom;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\n\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link SslComAcmeProviderTest}.\n */\npublic class SslComAcmeProviderTest {\n\n    private static final String PRODUCTION_ECC_DIRECTORY_URL = \"https://acme.ssl.com/sslcom-dv-ecc\";\n    private static final String PRODUCTION_RSA_DIRECTORY_URL = \"https://acme.ssl.com/sslcom-dv-rsa\";\n    private static final String STAGING_ECC_DIRECTORY_URL = \"https://acme-try.ssl.com/sslcom-dv-ecc\";\n    private static final String STAGING_RSA_DIRECTORY_URL = \"https://acme-try.ssl.com/sslcom-dv-rsa\";\n\n    /**\n     * Tests if the provider accepts the correct URIs.\n     */\n    @Test\n    public void testAccepts() throws URISyntaxException {\n        var provider = new SslComAcmeProvider();\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(provider.accepts(new URI(\"acme://ssl.com\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://ssl.com/\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://ssl.com/ecc\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://ssl.com/rsa\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://ssl.com/staging\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://ssl.com/staging/ecc\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://ssl.com/staging/rsa\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://example.com\"))).isFalse();\n            softly.assertThat(provider.accepts(new URI(\"http://example.com/acme\"))).isFalse();\n            softly.assertThat(provider.accepts(new URI(\"https://example.com/acme\"))).isFalse();\n        }\n    }\n\n    /**\n     * Test if acme URIs are properly resolved.\n     */\n    @Test\n    public void testResolve() throws URISyntaxException {\n        var provider = new SslComAcmeProvider();\n\n        assertThat(provider.resolve(new URI(\"acme://ssl.com\"))).isEqualTo(url(PRODUCTION_ECC_DIRECTORY_URL));\n        assertThat(provider.resolve(new URI(\"acme://ssl.com/\"))).isEqualTo(url(PRODUCTION_ECC_DIRECTORY_URL));\n        assertThat(provider.resolve(new URI(\"acme://ssl.com/ecc\"))).isEqualTo(url(PRODUCTION_ECC_DIRECTORY_URL));\n        assertThat(provider.resolve(new URI(\"acme://ssl.com/rsa\"))).isEqualTo(url(PRODUCTION_RSA_DIRECTORY_URL));\n        assertThat(provider.resolve(new URI(\"acme://ssl.com/staging\"))).isEqualTo(url(STAGING_ECC_DIRECTORY_URL));\n        assertThat(provider.resolve(new URI(\"acme://ssl.com/staging/ecc\"))).isEqualTo(url(STAGING_ECC_DIRECTORY_URL));\n        assertThat(provider.resolve(new URI(\"acme://ssl.com/staging/rsa\"))).isEqualTo(url(STAGING_RSA_DIRECTORY_URL));\n\n        assertThatIllegalArgumentException().isThrownBy(() -> provider.resolve(new URI(\"acme://ssl.com/v99\")));\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/provider/zerossl/ZeroSSLAcmeProviderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2024 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.provider.zerossl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\n\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link ZeroSSLAcmeProvider}.\n */\npublic class ZeroSSLAcmeProviderTest {\n\n    private static final String V02_DIRECTORY_URL = \"https://acme.zerossl.com/v2/DV90\";\n\n    /**\n     * Tests if the provider accepts the correct URIs.\n     */\n    @Test\n    public void testAccepts() throws URISyntaxException {\n        var provider = new ZeroSSLAcmeProvider();\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(provider.accepts(new URI(\"acme://zerossl.com\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://zerossl.com/\"))).isTrue();\n            softly.assertThat(provider.accepts(new URI(\"acme://example.com\"))).isFalse();\n            softly.assertThat(provider.accepts(new URI(\"http://example.com/acme\"))).isFalse();\n            softly.assertThat(provider.accepts(new URI(\"https://example.com/acme\"))).isFalse();\n        }\n    }\n\n    /**\n     * Test if acme URIs are properly resolved.\n     */\n    @Test\n    public void testResolve() throws URISyntaxException {\n        var provider = new ZeroSSLAcmeProvider();\n\n        assertThat(provider.resolve(new URI(\"acme://zerossl.com\"))).isEqualTo(url(V02_DIRECTORY_URL));\n        assertThat(provider.resolve(new URI(\"acme://zerossl.com/\"))).isEqualTo(url(V02_DIRECTORY_URL));\n\n        assertThrows(IllegalArgumentException.class, () -> provider.resolve(new URI(\"acme://zerossl.com/v99\")));\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/AcmeUtilsTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.toolbox;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.within;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.*;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStreamWriter;\nimport java.io.Writer;\nimport java.lang.reflect.Modifier;\nimport java.net.URI;\nimport java.security.Security;\nimport java.security.cert.CertificateEncodingException;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Locale;\nimport java.util.stream.Stream;\n\nimport org.bouncycastle.jce.provider.BouncyCastleProvider;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.junit.jupiter.params.provider.NullSource;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\n\n/**\n * Unit tests for {@link AcmeUtils}.\n */\npublic class AcmeUtilsTest {\n\n    @BeforeAll\n    public static void setup() {\n        Security.addProvider(new BouncyCastleProvider());\n    }\n\n    /**\n     * Test that constructor is private.\n     */\n    @Test\n    public void testPrivateConstructor() throws Exception {\n        var constructor = AcmeUtils.class.getDeclaredConstructor();\n        assertThat(Modifier.isPrivate(constructor.getModifiers())).isTrue();\n        constructor.setAccessible(true);\n        constructor.newInstance();\n    }\n\n    /**\n     * Test sha-256 hash and hex encode.\n     */\n    @Test\n    public void testSha256HashHexEncode() {\n        var hexEncode = hexEncode(sha256hash(\"foobar\"));\n        assertThat(hexEncode).isEqualTo(\"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2\");\n    }\n\n    /**\n     * Test base64 URL encode.\n     */\n    @Test\n    public void testBase64UrlEncode() {\n        var base64UrlEncode = base64UrlEncode(sha256hash(\"foobar\"));\n        assertThat(base64UrlEncode).isEqualTo(\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\");\n    }\n\n    /**\n     * Test base64 URL decode.\n     */\n    @Test\n    public void testBase64UrlDecode() {\n        var base64UrlDecode = base64UrlDecode(\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\");\n        assertThat(base64UrlDecode).isEqualTo(sha256hash(\"foobar\"));\n    }\n\n    /**\n     * Test base32 encode.\n     */\n    @ParameterizedTest\n    @CsvSource({    // Test vectors according to RFC 4648 section 10\n            \"'',''\",\n            \"f,MY======\",\n            \"fo,MZXQ====\",\n            \"foo,MZXW6===\",\n            \"foob,MZXW6YQ=\",\n            \"fooba,MZXW6YTB\",\n            \"foobar,MZXW6YTBOI======\",\n    })\n    public void testBase32Encode(String unencoded, String encoded) {\n        assertThat(base32Encode(unencoded.getBytes(UTF_8))).isEqualTo(encoded);\n    }\n\n    /**\n     * Test base64 URL validation for valid values\n     */\n    @ParameterizedTest\n    @ValueSource(strings = {\n            \"\",\n            \"Zg\",\n            \"Zm9v\",\n    })\n    public void testBase64UrlValid(String url) {\n        assertThat(isValidBase64Url(url)).isTrue();\n    }\n\n    /**\n     * Test base64 URL validation for invalid values\n     */\n    @ParameterizedTest\n    @ValueSource(strings = {\n            \"         \",\n            \"Zg=\",\n            \"Zg==\",\n            \"   Zm9v   \",\n            \"<some>.illegal#Text\",\n    })\n    @NullSource\n    public void testBase64UrlInvalid(String url) {\n        assertThat(isValidBase64Url(url)).isFalse();\n    }\n\n    /**\n     * Test ACE conversion.\n     */\n    @Test\n    public void testToAce() {\n        // Test ASCII domains in different notations\n        assertThat(toAce(\"example.com\")).isEqualTo(\"example.com\");\n        assertThat(toAce(\"   example.com  \")).isEqualTo(\"example.com\");\n        assertThat(toAce(\"ExAmPlE.CoM\")).isEqualTo(\"example.com\");\n        assertThat(toAce(\"foo.example.com\")).isEqualTo(\"foo.example.com\");\n        assertThat(toAce(\"bar.foo.example.com\")).isEqualTo(\"bar.foo.example.com\");\n\n        // Test IDN domains\n        assertThat(toAce(\"ExÄmþle.¢öM\")).isEqualTo(\"xn--exmle-hra7p.xn--m-7ba6w\");\n\n        // Test alternate separators\n        assertThat(toAce(\"example\\u3002com\")).isEqualTo(\"example.com\");\n        assertThat(toAce(\"example\\uff0ecom\")).isEqualTo(\"example.com\");\n        assertThat(toAce(\"example\\uff61com\")).isEqualTo(\"example.com\");\n\n        // Test ACE encoded domains, they must not change\n        assertThat(toAce(\"xn--exmle-hra7p.xn--m-7ba6w\"))\n                .isEqualTo(\"xn--exmle-hra7p.xn--m-7ba6w\");\n    }\n\n    /**\n     * Test valid strings.\n     */\n    @ParameterizedTest\n    @MethodSource(\"provideTimestamps\")\n    public void testParser(String input, String expected) {\n        Arguments.of(input, expected, within(1, ChronoUnit.MILLIS));\n    }\n\n    private static Stream<Arguments> provideTimestamps() {\n        return Stream.of(\n            Arguments.of(\"2015-12-27T22:58:35.006769519Z\", \"2015-12-27T22:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T22:58:35.00676951Z\", \"2015-12-27T22:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T22:58:35.0067695Z\", \"2015-12-27T22:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T22:58:35.006769Z\", \"2015-12-27T22:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T22:58:35.00676Z\", \"2015-12-27T22:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T22:58:35.0067Z\", \"2015-12-27T22:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T22:58:35.006Z\", \"2015-12-27T22:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T22:58:35.01Z\", \"2015-12-27T22:58:35.010Z\"),\n            Arguments.of(\"2015-12-27T22:58:35.2Z\", \"2015-12-27T22:58:35.200Z\"),\n            Arguments.of(\"2015-12-27T22:58:35Z\", \"2015-12-27T22:58:35.000Z\"),\n            Arguments.of(\"2015-12-27t22:58:35z\", \"2015-12-27T22:58:35.000Z\"),\n\n            Arguments.of(\"2015-12-27T22:58:35.006769519+02:00\", \"2015-12-27T20:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T22:58:35.006+02:00\", \"2015-12-27T20:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T22:58:35+02:00\", \"2015-12-27T20:58:35.000Z\"),\n\n            Arguments.of(\"2015-12-27T21:58:35.006769519-02:00\", \"2015-12-27T23:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T21:58:35.006-02:00\", \"2015-12-27T23:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T21:58:35-02:00\", \"2015-12-27T23:58:35.000Z\"),\n\n            Arguments.of(\"2015-12-27T22:58:35.006769519+0200\", \"2015-12-27T20:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T22:58:35.006+0200\", \"2015-12-27T20:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T22:58:35+0200\", \"2015-12-27T20:58:35.000Z\"),\n\n            Arguments.of(\"2015-12-27T21:58:35.006769519-0200\", \"2015-12-27T23:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T21:58:35.006-0200\", \"2015-12-27T23:58:35.006Z\"),\n            Arguments.of(\"2015-12-27T21:58:35-0200\", \"2015-12-27T23:58:35.000Z\")\n        );\n    }\n\n    /**\n     * Test invalid strings.\n     */\n    @Test\n    public void testInvalid() {\n        assertThrows(IllegalArgumentException.class,\n                () -> parseTimestamp(\"\"),\n                \"accepted empty string\");\n        assertThrows(IllegalArgumentException.class,\n                () -> parseTimestamp(\"abc\"),\n                \"accepted nonsense string\");\n        assertThrows(IllegalArgumentException.class,\n                () -> parseTimestamp(\"2015-12-27\"),\n                \"accepted date only string\");\n        assertThrows(IllegalArgumentException.class,\n                () -> parseTimestamp(\"2015-12-27T\"),\n                \"accepted string without time\");\n    }\n\n    /**\n     * Test that locales are correctly converted to language headers.\n     */\n    @Test\n    public void testLocaleToLanguageHeader() {\n        assertThat(localeToLanguageHeader(Locale.ENGLISH))\n                .isEqualTo(\"en,*;q=0.1\");\n        assertThat(localeToLanguageHeader(new Locale(\"en\", \"US\")))\n                .isEqualTo(\"en-US,en;q=0.8,*;q=0.1\");\n        assertThat(localeToLanguageHeader(Locale.GERMAN))\n                .isEqualTo(\"de,*;q=0.1\");\n        assertThat(localeToLanguageHeader(Locale.GERMANY))\n                .isEqualTo(\"de-DE,de;q=0.8,*;q=0.1\");\n        assertThat(localeToLanguageHeader(new Locale(\"\")))\n                .isEqualTo(\"*\");\n        assertThat(localeToLanguageHeader(null))\n                .isEqualTo(\"*\");\n    }\n\n    /**\n     * Test that error prefix is correctly removed.\n     */\n    @Test\n    public void testStripErrorPrefix() {\n        assertThat(stripErrorPrefix(\"urn:ietf:params:acme:error:unauthorized\")).isEqualTo(\"unauthorized\");\n        assertThat(stripErrorPrefix(\"urn:somethingelse:error:message\")).isNull();\n        assertThat(stripErrorPrefix(null)).isNull();\n    }\n\n    /**\n     * Test that {@link AcmeUtils#writeToPem(byte[], PemLabel, Writer)} writes a correct PEM\n     * file.\n     */\n    @Test\n    public void testWriteToPem() throws IOException, CertificateEncodingException {\n        var certChain = TestUtils.createCertificate(\"/cert.pem\");\n\n        var pemFile = new ByteArrayOutputStream();\n        try (var w = new OutputStreamWriter(pemFile)) {\n            for (var cert : certChain) {\n                AcmeUtils.writeToPem(cert.getEncoded(), AcmeUtils.PemLabel.CERTIFICATE, w);\n            }\n        }\n\n        var originalFile = new ByteArrayOutputStream();\n        try (var in = getClass().getResourceAsStream(\"/cert.pem\")) {\n            var buffer = new byte[2048];\n            int len;\n            while ((len = in.read(buffer)) >= 0) {\n                originalFile.write(buffer, 0, len);\n            }\n        }\n\n        assertThat(pemFile.toByteArray()).isEqualTo(originalFile.toByteArray());\n    }\n\n    /**\n     * Test {@link AcmeUtils#getContentType(String)} for JSON types.\n     */\n    @ParameterizedTest\n    @ValueSource(strings = {\n            \"application/json\",\n            \"application/json; charset=utf-8\",\n            \"application/json; charset=utf-8 (Plain text)\",\n            \"application/json; charset=\\\"utf-8\\\"\",\n            \"application/json; charset=\\\"UTF-8\\\"; foo=4\",\n            \" application/json ;foo=4\",\n    })\n    public void testGetContentTypeForJson(String contentType) {\n        assertThat(AcmeUtils.getContentType(contentType)).isEqualTo(\"application/json\");\n    }\n\n    /**\n     * Test {@link AcmeUtils#getContentType(String)} with other types.\n     */\n    @Test\n    public void testGetContentType() {\n        assertThat(AcmeUtils.getContentType(null)).isNull();\n        assertThat(AcmeUtils.getContentType(\"Application/Problem+JSON\"))\n                .isEqualTo(\"application/problem+json\");\n        assertThrows(AcmeProtocolException.class,\n                () -> AcmeUtils.getContentType(\"application/json; charset=\\\"iso-8859-1\\\"\"));\n    }\n\n    /**\n     * Test that {@link AcmeUtils#validateContact(java.net.URI)} refuses invalid\n     * contacts.\n     */\n    @Test\n    public void testValidateContact() {\n        AcmeUtils.validateContact(URI.create(\"mailto:foo@example.com\"));\n\n        assertThrows(IllegalArgumentException.class,\n                () -> AcmeUtils.validateContact(URI.create(\"mailto:foo@example.com,bar@example.com\")),\n                \"multiple recipients are accepted\");\n        assertThrows(IllegalArgumentException.class,\n                () -> AcmeUtils.validateContact(URI.create(\"mailto:foo@example.com?to=bar@example.com\")),\n                \"hfields are accepted\");\n        assertThrows(IllegalArgumentException.class,\n                () -> AcmeUtils.validateContact(URI.create(\"mailto:?to=foo@example.com\")),\n                \"only hfields are accepted\");\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/JSONBuilderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.toolbox;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.time.Duration;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Map;\n\nimport org.jose4j.json.JsonUtil;\nimport org.jose4j.lang.JoseException;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit test for {@link JSONBuilder}.\n */\npublic class JSONBuilderTest {\n\n    /**\n     * Test that an empty JSON builder is empty.\n     */\n    @Test\n    public void testEmpty() {\n        var cb = new JSONBuilder();\n        assertThat(cb.toString()).isEqualTo(\"{}\");\n    }\n\n    /**\n     * Test basic data types. Also test that methods return {@code this}, and that\n     * existing keys are replaced.\n     */\n    @Test\n    public void testBasics() {\n        JSONBuilder res;\n\n        var cb = new JSONBuilder();\n        res = cb.put(\"fooStr\", \"String\");\n        assertThat(res).isSameAs(cb);\n\n        res = cb.put(\"fooInt\", 123);\n        assertThat(res).isSameAs(cb);\n\n        res = cb.put(\"fooInt\", 456);\n        assertThat(res).isSameAs(cb);\n\n        assertThat(cb.toString()).isEqualTo(\"{\\\"fooStr\\\":\\\"String\\\",\\\"fooInt\\\":456}\");\n\n        var map = cb.toMap();\n        assertThat(map.keySet()).hasSize(2);\n        assertThat(map).extracting(\"fooInt\").isEqualTo(456);\n        assertThat(map).extracting(\"fooStr\").isEqualTo(\"String\");\n\n        var json = cb.toJSON();\n        assertThat(json.keySet()).hasSize(2);\n        assertThat(json.get(\"fooInt\").asInt()).isEqualTo(456);\n        assertThat(json.get(\"fooStr\").asString()).isEqualTo(\"String\");\n    }\n\n    /**\n     * Test date type.\n     */\n    @Test\n    public void testDate() {\n        var date = ZonedDateTime.of(2016, 6, 1, 5, 13, 46, 0, ZoneId.of(\"GMT+2\")).toInstant();\n        var duration = Duration.ofMinutes(5);\n\n        var cb = new JSONBuilder();\n        cb.put(\"fooDate\", date);\n        cb.put(\"fooDuration\", duration);\n        cb.put(\"fooNull\", (Object) null);\n\n        assertThat(cb.toString()).isEqualTo(\"{\\\"fooDate\\\":\\\"2016-06-01T03:13:46Z\\\",\\\"fooDuration\\\":300,\\\"fooNull\\\":null}\");\n    }\n\n    /**\n     * Test base64 encoding.\n     */\n    @Test\n    public void testBase64() {\n        var data = \"abc123\".getBytes();\n\n        JSONBuilder res;\n\n        var cb = new JSONBuilder();\n        res = cb.putBase64(\"foo\", data);\n        assertThat(res).isSameAs(cb);\n        assertThat(cb.toString()).isEqualTo(\"{\\\"foo\\\":\\\"YWJjMTIz\\\"}\");\n    }\n\n    /**\n     * Test JWK.\n     */\n    @Test\n    public void testKey() throws IOException, JoseException {\n        var keyPair = TestUtils.createKeyPair();\n\n        JSONBuilder res;\n\n        var cb = new JSONBuilder();\n        res = cb.putKey(\"foo\", keyPair.getPublic());\n        assertThat(res).isSameAs(cb);\n\n        var json = JsonUtil.parseJson(cb.toString());\n        assertThat(json).containsKey(\"foo\");\n\n        var jwk = (Map<String, String>) json.get(\"foo\");\n        assertThat(jwk.keySet()).hasSize(3);\n        assertThat(jwk).extracting(\"n\").isEqualTo(TestUtils.N);\n        assertThat(jwk).extracting(\"e\").isEqualTo(TestUtils.E);\n        assertThat(jwk).extracting(\"kty\").isEqualTo(TestUtils.KTY);\n    }\n\n    /**\n     * Test sub claims (objects).\n     */\n    @Test\n    public void testObject() {\n        var cb = new JSONBuilder();\n        var sub = cb.object(\"sub\");\n        assertThat(sub).isNotSameAs(cb);\n\n        assertThat(cb.toString()).isEqualTo(\"{\\\"sub\\\":{}}\");\n\n        cb.put(\"foo\", 123);\n        sub.put(\"foo\", 456);\n\n        assertThat(cb.toString()).isEqualTo(\"{\\\"sub\\\":{\\\"foo\\\":456},\\\"foo\\\":123}\");\n    }\n\n    /**\n     * Test arrays.\n     */\n    @Test\n    public void testArray() {\n        JSONBuilder res;\n\n        var cb1 = new JSONBuilder();\n        res = cb1.array(\"ar\", Collections.emptyList());\n        assertThat(res).isSameAs(cb1);\n        assertThat(cb1.toString()).isEqualTo(\"{\\\"ar\\\":[]}\");\n\n        var cb2 = new JSONBuilder();\n        res = cb2.array(\"ar\", Collections.singletonList(123));\n        assertThat(res).isSameAs(cb2);\n        assertThat(cb2.toString()).isEqualTo(\"{\\\"ar\\\":[123]}\");\n\n        var cb3 = new JSONBuilder();\n        res = cb3.array(\"ar\", Arrays.asList(123, \"foo\", 456));\n        assertThat(res).isSameAs(cb3);\n        assertThat(cb3.toString()).isEqualTo(\"{\\\"ar\\\":[123,\\\"foo\\\",456]}\");\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/JSONTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2016 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.toolbox;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatExceptionOfType;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\nimport java.net.URI;\nimport java.net.URL;\nimport java.time.LocalDate;\nimport java.time.ZoneId;\nimport java.util.ArrayList;\nimport java.util.NoSuchElementException;\nimport java.util.stream.Collectors;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.exception.AcmeNotSupportedException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSON.Value;\n\n/**\n * Unit test for {@link JSON}.\n */\npublic class JSONTest {\n\n    private static final URL BASE_URL = url(\"https://example.com/acme/1\");\n\n    /**\n     * Test that an empty {@link JSON} is empty.\n     */\n    @Test\n    public void testEmpty() {\n        var empty = JSON.empty();\n        assertThat(empty.toString()).isEqualTo(\"{}\");\n        assertThat(empty.toMap().keySet()).isEmpty();\n    }\n\n    /**\n     * Test parsers.\n     */\n    @Test\n    public void testParsers() throws IOException {\n        String json = \"{\\\"foo\\\":\\\"a-text\\\",\\n\\\"bar\\\":123}\";\n\n        var fromString = JSON.parse(json);\n        assertThatJson(fromString.toString()).isEqualTo(json);\n        var map = fromString.toMap();\n        assertThat(map).hasSize(2);\n        assertThat(map.keySet()).containsExactlyInAnyOrder(\"foo\", \"bar\");\n        assertThat(map.get(\"foo\")).isEqualTo(\"a-text\");\n        assertThat(map.get(\"bar\")).isEqualTo(123L);\n\n        try (var in = new ByteArrayInputStream(json.getBytes(UTF_8))) {\n            var fromStream = JSON.parse(in);\n            assertThatJson(fromStream.toString()).isEqualTo(json);\n            var map2 = fromStream.toMap();\n            assertThat(map2).hasSize(2);\n            assertThat(map2.keySet()).containsExactlyInAnyOrder(\"foo\", \"bar\");\n            assertThat(map2.get(\"foo\")).isEqualTo(\"a-text\");\n            assertThat(map2.get(\"bar\")).isEqualTo(123L);\n        }\n    }\n\n    /**\n     * Test that bad JSON fails.\n     */\n    @Test\n    public void testParsersBadJSON() {\n        assertThrows(AcmeProtocolException.class,\n                () -> JSON.parse(\"This is no JSON.\")\n        );\n    }\n\n    /**\n     * Test all object related methods.\n     */\n    @Test\n    public void testObject() {\n        var json = TestUtils.getJSON(\"datatypes\");\n\n        assertThat(json.keySet()).containsExactlyInAnyOrder(\n                    \"text\", \"number\", \"boolean\", \"uri\", \"url\", \"date\", \"array\",\n                    \"collect\", \"status\", \"binary\", \"duration\", \"problem\", \"encoded\");\n        assertThat(json.contains(\"text\")).isTrue();\n        assertThat(json.contains(\"music\")).isFalse();\n        assertThat(json.get(\"text\")).isNotNull();\n        assertThat(json.get(\"music\")).isNotNull();\n    }\n\n    /**\n     * Test all array related methods.\n     */\n    @Test\n    public void testArray() {\n        var json = TestUtils.getJSON(\"datatypes\");\n        var array = json.get(\"array\").asArray();\n\n        assertThat(array.isEmpty()).isFalse();\n        assertThat(array).hasSize(4).doesNotContainNull();\n    }\n\n    /**\n     * Test empty array.\n     */\n    @Test\n    public void testEmptyArray() {\n        var json = TestUtils.getJSON(\"datatypes\");\n        var array = json.get(\"missingArray\").asArray();\n\n        assertThat(array.isEmpty()).isTrue();\n        assertThat(array).hasSize(0);\n        assertThat(array.stream().count()).isEqualTo(0L);\n    }\n\n    /**\n     * Test all array iterator related methods.\n     */\n    @Test\n    public void testArrayIterator() {\n        var json = TestUtils.getJSON(\"datatypes\");\n        var array = json.get(\"array\").asArray();\n\n        var it = array.iterator();\n        assertThat(it).isNotNull();\n\n        assertThat(it.hasNext()).isTrue();\n        assertThat(it.next().asString()).isEqualTo(\"foo\");\n\n        assertThat(it.hasNext()).isTrue();\n        assertThat(it.next().asInt()).isEqualTo(987);\n\n        assertThat(it.hasNext()).isTrue();\n        assertThat(it.next().asArray()).hasSize(3);\n\n        assertThat(it.hasNext()).isTrue();\n        assertThrows(UnsupportedOperationException.class, it::remove);\n        assertThat(it.next().asObject()).isNotNull();\n\n        assertThat(it.hasNext()).isFalse();\n        assertThrows(NoSuchElementException.class, it::next);\n    }\n\n    /**\n     * Test the array stream.\n     */\n    @Test\n    public void testArrayStream() {\n        var json = TestUtils.getJSON(\"datatypes\");\n        var array = json.get(\"array\").asArray();\n\n        var streamValues = array.stream().collect(Collectors.toList());\n\n        var iteratorValues = new ArrayList<JSON.Value>();\n        for (var value : array) {\n            iteratorValues.add(value);\n        }\n\n        assertThat(streamValues).containsAll(iteratorValues);\n    }\n\n    /**\n     * Test all getters on existing values.\n     */\n    @Test\n    public void testGetter() {\n        var date = LocalDate.of(2016, 1, 8).atStartOfDay(ZoneId.of(\"UTC\")).toInstant();\n\n        var json = TestUtils.getJSON(\"datatypes\");\n\n        assertThat(json.get(\"text\").asString()).isEqualTo(\"lorem ipsum\");\n        assertThat(json.get(\"number\").asInt()).isEqualTo(123);\n        assertThat(json.get(\"boolean\").asBoolean()).isTrue();\n        assertThat(json.get(\"uri\").asURI()).isEqualTo(URI.create(\"mailto:foo@example.com\"));\n        assertThat(json.get(\"url\").asURL()).isEqualTo(url(\"http://example.com\"));\n        assertThat(json.get(\"date\").asInstant()).isEqualTo(date);\n        assertThat(json.get(\"status\").asStatus()).isEqualTo(Status.VALID);\n        assertThat(json.get(\"binary\").asBinary()).isEqualTo(\"Chainsaw\".getBytes());\n        assertThat(json.get(\"duration\").asDuration()).hasSeconds(86400L);\n\n        assertThat(json.get(\"text\").isPresent()).isTrue();\n        assertThat(json.get(\"text\").optional().isPresent()).isTrue();\n        assertThat(json.get(\"text\").map(Value::asString).isPresent()).isTrue();\n\n        var array = json.get(\"array\").asArray();\n        assertThat(array.get(0).asString()).isEqualTo(\"foo\");\n        assertThat(array.get(1).asInt()).isEqualTo(987);\n\n        var array2 = array.get(2).asArray();\n        assertThat(array2.get(0).asInt()).isEqualTo(1);\n        assertThat(array2.get(1).asInt()).isEqualTo(2);\n        assertThat(array2.get(2).asInt()).isEqualTo(3);\n\n        var sub = array.get(3).asObject();\n        assertThat(sub.get(\"test\").asString()).isEqualTo(\"ok\");\n\n        var encodedSub = json.get(\"encoded\").asEncodedObject();\n        assertThatJson(encodedSub.toString()).isEqualTo(\"{\\\"key\\\":\\\"value\\\"}\");\n\n        var problem = json.get(\"problem\").asProblem(BASE_URL);\n        assertThat(problem).isNotNull();\n        assertThat(problem.getType()).isEqualTo(URI.create(\"urn:ietf:params:acme:error:rateLimited\"));\n        assertThat(problem.getDetail().orElseThrow()).isEqualTo(\"too many requests\");\n        assertThat(problem.getInstance().orElseThrow())\n                .isEqualTo(URI.create(\"https://example.com/documents/errors.html\"));\n    }\n\n    /**\n     * Test that getters are null safe.\n     */\n    @Test\n    public void testNullGetter() {\n        var json = TestUtils.getJSON(\"datatypes\");\n\n        assertThat(json.get(\"none\")).isNotNull();\n        assertThat(json.get(\"none\").isPresent()).isFalse();\n        assertThat(json.get(\"none\").optional().isPresent()).isFalse();\n        assertThat(json.get(\"none\").map(Value::asString).isPresent()).isFalse();\n\n        assertThatExceptionOfType(AcmeNotSupportedException.class)\n                .isThrownBy(() -> json.getFeature(\"none\"))\n                .withMessage(\"Server does not support none\");\n\n        assertThatExceptionOfType(AcmeNotSupportedException.class)\n                .isThrownBy(() -> json.get(\"none\").onFeature(\"my-feature\"))\n                .withMessage(\"Server does not support my-feature\");\n\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"none\").asString(),\n                \"asString\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"none\").asURI(),\n                \"asURI\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"none\").asURL(),\n                \"asURL\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"none\").asInstant(),\n                \"asInstant\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"none\").asDuration(),\n                \"asDuration\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"none\").asObject(),\n                \"asObject\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"none\").asEncodedObject(),\n                \"asEncodedObject\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"none\").asStatus(),\n                \"asStatus\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"none\").asBinary(),\n                \"asBinary\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"none\").asProblem(BASE_URL),\n                \"asProblem\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"none\").asInt(),\n                \"asInt\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"none\").asBoolean(),\n                \"asBoolean\");\n    }\n\n    /**\n     * Test that wrong getters return an exception.\n     */\n    @Test\n    public void testWrongGetter() {\n        var json = TestUtils.getJSON(\"datatypes\");\n\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"text\").asObject(),\n                \"asObject\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"text\").asEncodedObject(),\n                \"asEncodedObject\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"text\").asArray(),\n                \"asArray\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"text\").asInt(),\n                \"asInt\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"text\").asURI(),\n                \"asURI\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"text\").asURL(),\n                \"asURL\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"text\").asInstant(),\n                \"asInstant\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"text\").asDuration(),\n                \"asDuration\");\n        assertThrows(AcmeProtocolException.class,\n                () -> json.get(\"text\").asProblem(BASE_URL),\n                \"asProblem\");\n    }\n\n    /**\n     * Test that serialization works correctly.\n     */\n    @Test\n    public void testSerialization() throws IOException, ClassNotFoundException {\n        var originalJson = TestUtils.getJSON(\"newAuthorizationResponse\");\n\n        // Serialize\n        byte[] data;\n        try (var out = new ByteArrayOutputStream()) {\n            try (var oos = new ObjectOutputStream(out)) {\n                oos.writeObject(originalJson);\n            }\n            data = out.toByteArray();\n        }\n\n        // Deserialize\n        JSON testJson;\n        try (var in = new ByteArrayInputStream(data)) {\n            try (var ois = new ObjectInputStream(in)) {\n                testJson = (JSON) ois.readObject();\n            }\n        }\n\n        assertThat(testJson).isNotSameAs(originalJson);\n        assertThat(testJson.toString()).isNotEmpty();\n        assertThatJson(testJson.toString()).isEqualTo(originalJson.toString());\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/JoseUtilsTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2019 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.toolbox;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.fail;\nimport static org.shredzone.acme4j.toolbox.TestUtils.url;\n\nimport java.net.URL;\nimport java.util.Base64;\nimport java.util.HashMap;\n\nimport javax.crypto.SecretKey;\n\nimport org.jose4j.jwk.PublicJsonWebKey;\nimport org.jose4j.jws.JsonWebSignature;\nimport org.jose4j.jwx.CompactSerializer;\nimport org.jose4j.lang.JoseException;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\n\n/**\n * Unit tests for {@link JoseUtils}.\n */\npublic class JoseUtilsTest {\n\n    private static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder().withoutPadding();\n    private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder();\n\n    /**\n     * Test if a JOSE ACME POST request is correctly created.\n     */\n    @Test\n    public void testCreateJosePostRequest() throws Exception {\n        var resourceUrl = url(\"http://example.com/acme/resource\");\n        var accountKey = TestUtils.createKeyPair();\n        var nonce = URL_ENCODER.encodeToString(\"foo-nonce-1-foo\".getBytes());\n        var payload = new JSONBuilder();\n        payload.put(\"foo\", 123);\n        payload.put(\"bar\", \"a-string\");\n\n        var jose = JoseUtils\n                .createJoseRequest(resourceUrl, accountKey, payload, nonce, TestUtils.ACCOUNT_URL)\n                .toMap();\n\n        var encodedHeader = jose.get(\"protected\").toString();\n        var encodedSignature = jose.get(\"signature\").toString();\n        var encodedPayload = jose.get(\"payload\").toString();\n\n        var expectedHeader = new StringBuilder();\n        expectedHeader.append('{');\n        expectedHeader.append(\"\\\"nonce\\\":\\\"\").append(nonce).append(\"\\\",\");\n        expectedHeader.append(\"\\\"url\\\":\\\"\").append(resourceUrl).append(\"\\\",\");\n        expectedHeader.append(\"\\\"alg\\\":\\\"RS256\\\",\");\n        expectedHeader.append(\"\\\"kid\\\":\\\"\").append(TestUtils.ACCOUNT_URL).append('\"');\n        expectedHeader.append('}');\n\n        assertThatJson(new String(URL_DECODER.decode(encodedHeader), UTF_8))\n                .isEqualTo(expectedHeader.toString());\n        assertThatJson(new String(URL_DECODER.decode(encodedPayload), UTF_8))\n                .isEqualTo(\"{\\\"foo\\\":123,\\\"bar\\\":\\\"a-string\\\"}\");\n        assertThat(encodedSignature).isNotEmpty();\n\n        var jws = new JsonWebSignature();\n        jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature));\n        jws.setKey(accountKey.getPublic());\n        assertThat(jws.verifySignature()).isTrue();\n    }\n\n    /**\n     * Test if a JOSE ACME POST-as-GET request is correctly created.\n     */\n    @Test\n    public void testCreateJosePostAsGetRequest() throws Exception {\n        var resourceUrl = url(\"http://example.com/acme/resource\");\n        var accountKey = TestUtils.createKeyPair();\n        var nonce = URL_ENCODER.encodeToString(\"foo-nonce-1-foo\".getBytes());\n\n        var jose = JoseUtils\n                .createJoseRequest(resourceUrl, accountKey, null, nonce, TestUtils.ACCOUNT_URL)\n                .toMap();\n\n        var encodedHeader = jose.get(\"protected\").toString();\n        var encodedSignature = jose.get(\"signature\").toString();\n        var encodedPayload = jose.get(\"payload\").toString();\n\n        var expectedHeader = new StringBuilder();\n        expectedHeader.append('{');\n        expectedHeader.append(\"\\\"nonce\\\":\\\"\").append(nonce).append(\"\\\",\");\n        expectedHeader.append(\"\\\"url\\\":\\\"\").append(resourceUrl).append(\"\\\",\");\n        expectedHeader.append(\"\\\"alg\\\":\\\"RS256\\\",\");\n        expectedHeader.append(\"\\\"kid\\\":\\\"\").append(TestUtils.ACCOUNT_URL).append('\"');\n        expectedHeader.append('}');\n\n        assertThatJson(new String(URL_DECODER.decode(encodedHeader), UTF_8))\n                .isEqualTo(expectedHeader.toString());\n        assertThat(new String(URL_DECODER.decode(encodedPayload), UTF_8)).isEmpty();\n        assertThat(encodedSignature).isNotEmpty();\n\n        var jws = new JsonWebSignature();\n        jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature));\n        jws.setKey(accountKey.getPublic());\n        assertThat(jws.verifySignature()).isTrue();\n    }\n\n    /**\n     * Test if a JOSE ACME Key-Change request is correctly created.\n     */\n    @Test\n    public void testCreateJoseKeyChangeRequest() throws Exception {\n        var resourceUrl = url(\"http://example.com/acme/resource\");\n        var accountKey = TestUtils.createKeyPair();\n        var payload = new JSONBuilder();\n        payload.put(\"foo\", 123);\n        payload.put(\"bar\", \"a-string\");\n\n        var jose = JoseUtils\n                .createJoseRequest(resourceUrl, accountKey, payload, null, null)\n                .toMap();\n\n        var encodedHeader = jose.get(\"protected\").toString();\n        var encodedSignature = jose.get(\"signature\").toString();\n        var encodedPayload = jose.get(\"payload\").toString();\n\n        var expectedHeader = new StringBuilder();\n        expectedHeader.append('{');\n        expectedHeader.append(\"\\\"url\\\":\\\"\").append(resourceUrl).append(\"\\\",\");\n        expectedHeader.append(\"\\\"alg\\\":\\\"RS256\\\",\");\n        expectedHeader.append(\"\\\"jwk\\\": {\");\n        expectedHeader.append(\"\\\"kty\\\": \\\"\").append(TestUtils.KTY).append(\"\\\",\");\n        expectedHeader.append(\"\\\"e\\\": \\\"\").append(TestUtils.E).append(\"\\\",\");\n        expectedHeader.append(\"\\\"n\\\": \\\"\").append(TestUtils.N).append(\"\\\"}\");\n        expectedHeader.append(\"}\");\n\n        assertThatJson(new String(URL_DECODER.decode(encodedHeader), UTF_8))\n                .isEqualTo(expectedHeader.toString());\n        assertThatJson(new String(URL_DECODER.decode(encodedPayload), UTF_8))\n                .isEqualTo(\"{\\\"foo\\\":123,\\\"bar\\\":\\\"a-string\\\"}\");\n        assertThat(encodedSignature).isNotEmpty();\n\n        var jws = new JsonWebSignature();\n        jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature));\n        jws.setKey(accountKey.getPublic());\n        assertThat(jws.verifySignature()).isTrue();\n    }\n\n    /**\n     * Test if an external account binding is correctly created.\n     */\n    @ParameterizedTest\n    @CsvSource({\"SHA-256,HS256\", \"SHA-384,HS384\", \"SHA-512,HS512\", \"SHA-512,HS256\"})\n    public void testCreateExternalAccountBinding(String keyAlg, String macAlg) throws Exception {\n        var accountKey = TestUtils.createKeyPair();\n        var keyIdentifier = \"NCC-1701\";\n        var macKey = TestUtils.createSecretKey(keyAlg);\n        var resourceUrl = url(\"http://example.com/acme/resource\");\n\n        var binding = JoseUtils.createExternalAccountBinding(\n                keyIdentifier, accountKey.getPublic(), macKey, macAlg, resourceUrl);\n\n        var encodedHeader = binding.get(\"protected\").toString();\n        var encodedSignature = binding.get(\"signature\").toString();\n        var encodedPayload = binding.get(\"payload\").toString();\n        var serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);\n\n        assertExternalAccountBinding(serialized, resourceUrl, keyIdentifier, macKey, macAlg);\n    }\n\n    /**\n     * Test if public key is correctly converted to JWK structure.\n     */\n    @Test\n    public void testPublicKeyToJWK() throws Exception {\n        var json = JoseUtils.publicKeyToJWK(TestUtils.createKeyPair().getPublic());\n        assertThat(json).hasSize(3);\n        assertThat(json.get(\"kty\")).isEqualTo(TestUtils.KTY);\n        assertThat(json.get(\"n\")).isEqualTo(TestUtils.N);\n        assertThat(json.get(\"e\")).isEqualTo(TestUtils.E);\n    }\n\n    /**\n     * Test if JWK structure is correctly converted to public key.\n     */\n    @Test\n    public void testJWKToPublicKey() throws Exception {\n        var json = new HashMap<String, Object>();\n        json.put(\"kty\", TestUtils.KTY);\n        json.put(\"n\", TestUtils.N);\n        json.put(\"e\", TestUtils.E);\n        var key = JoseUtils.jwkToPublicKey(json);\n        assertThat(key.getEncoded()).isEqualTo(TestUtils.createKeyPair().getPublic().getEncoded());\n    }\n\n    /**\n     * Test if thumbprint is correctly computed.\n     */\n    @Test\n    public void testThumbprint() throws Exception {\n        var thumb = JoseUtils.thumbprint(TestUtils.createKeyPair().getPublic());\n        var encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(thumb);\n        assertThat(encoded).isEqualTo(TestUtils.THUMBPRINT);\n    }\n\n    /**\n     * Test if RSA using SHA-256 keys are properly detected.\n     */\n    @Test\n    public void testRsaKey() throws Exception {\n        var rsaKeyPair = TestUtils.createKeyPair();\n        var jwk = PublicJsonWebKey.Factory.newPublicJwk(rsaKeyPair.getPublic());\n\n        var type = JoseUtils.keyAlgorithm(jwk);\n        assertThat(type).isEqualTo(\"RS256\");\n    }\n\n    /**\n     * Test if ECDSA using NIST P-256 curve and SHA-256 keys are properly detected.\n     */\n    @Test\n    public void testP256ECKey() throws Exception {\n        var ecKeyPair = TestUtils.createECKeyPair(\"secp256r1\");\n        var jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic());\n\n        var type = JoseUtils.keyAlgorithm(jwk);\n        assertThat(type).isEqualTo(\"ES256\");\n    }\n\n    /**\n     * Test if ECDSA using NIST P-384 curve and SHA-384 keys are properly detected.\n     */\n    @Test\n    public void testP384ECKey() throws Exception {\n        var ecKeyPair = TestUtils.createECKeyPair(\"secp384r1\");\n        var jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic());\n\n        var type = JoseUtils.keyAlgorithm(jwk);\n        assertThat(type).isEqualTo(\"ES384\");\n    }\n\n    /**\n     * Test if ECDSA using NIST P-521 curve and SHA-512 keys are properly detected.\n     */\n    @Test\n    public void testP521ECKey() throws Exception {\n        var ecKeyPair = TestUtils.createECKeyPair(\"secp521r1\");\n        var jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic());\n\n        var type = JoseUtils.keyAlgorithm(jwk);\n        assertThat(type).isEqualTo(\"ES512\");\n    }\n\n    /**\n     * Test if MAC key algorithms are properly detected.\n     */\n    @Test\n    public void testMacKey() throws Exception {\n        assertThat(JoseUtils.macKeyAlgorithm(TestUtils.createSecretKey(\"SHA-256\"))).isEqualTo(\"HS256\");\n        assertThat(JoseUtils.macKeyAlgorithm(TestUtils.createSecretKey(\"SHA-384\"))).isEqualTo(\"HS384\");\n        assertThat(JoseUtils.macKeyAlgorithm(TestUtils.createSecretKey(\"SHA-512\"))).isEqualTo(\"HS512\");\n    }\n\n    /**\n     * Asserts that the serialized external account binding is valid. Unit test fails if\n     * the account binding is invalid.\n     *\n     * @param serialized\n     *         Serialized external account binding JOSE structure\n     * @param resourceUrl\n     *         Expected resource {@link URL}\n     * @param keyIdentifier\n     *         Expected key identifier\n     * @param macKey\n     *         Expected {@link SecretKey}\n     * @param macAlg\n     *         Expected algorithm\n     */\n    public static void assertExternalAccountBinding(String serialized, URL resourceUrl,\n                                                    String keyIdentifier, SecretKey macKey,\n                                                    String macAlg) {\n        try {\n            var jws = new JsonWebSignature();\n            jws.setCompactSerialization(serialized);\n            jws.setKey(macKey);\n            assertThat(jws.verifySignature()).isTrue();\n\n            assertThat(jws.getHeader(\"url\")).isEqualTo(resourceUrl.toString());\n            assertThat(jws.getHeader(\"kid\")).isEqualTo(keyIdentifier);\n            assertThat(jws.getHeader(\"alg\")).isEqualTo(macAlg);\n\n            var decodedPayload = jws.getPayload();\n            var expectedPayload = new StringBuilder();\n            expectedPayload.append('{');\n            expectedPayload.append(\"\\\"kty\\\":\\\"\").append(TestUtils.KTY).append(\"\\\",\");\n            expectedPayload.append(\"\\\"e\\\":\\\"\").append(TestUtils.E).append(\"\\\",\");\n            expectedPayload.append(\"\\\"n\\\":\\\"\").append(TestUtils.N).append(\"\\\"\");\n            expectedPayload.append(\"}\");\n            assertThatJson(decodedPayload).isEqualTo(expectedPayload.toString());\n        } catch (JoseException ex) {\n            fail(ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/TestUtils.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.toolbox;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static java.util.stream.Collectors.toUnmodifiableList;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.security.InvalidAlgorithmParameterException;\nimport java.security.KeyFactory;\nimport java.security.KeyPair;\nimport java.security.KeyPairGenerator;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\nimport java.security.cert.CertificateException;\nimport java.security.cert.CertificateFactory;\nimport java.security.cert.X509Certificate;\nimport java.security.spec.ECGenParameterSpec;\nimport java.security.spec.InvalidKeySpecException;\nimport java.security.spec.PKCS8EncodedKeySpec;\nimport java.security.spec.X509EncodedKeySpec;\nimport java.util.Base64;\nimport java.util.List;\nimport java.util.TreeMap;\n\nimport javax.crypto.SecretKey;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.jose4j.json.JsonUtil;\nimport org.jose4j.jwk.JsonWebKey;\nimport org.jose4j.jwk.JsonWebKey.OutputControlLevel;\nimport org.jose4j.keys.HmacKey;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Problem;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.connector.Connection;\nimport org.shredzone.acme4j.connector.NetworkSettings;\nimport org.shredzone.acme4j.provider.AcmeProvider;\n\n/**\n * Some utility methods for unit tests.\n */\npublic final class TestUtils {\n    public static final String N = \"pZsTKY41y_CwgJ0VX7BmmGs_7UprmXQMGPcnSbBeJAjZHA9SyyJKaWv4fNUdBIAX3Y2QoZixj50nQLyLv2ng3pvEoRL0sx9ZHgp5ndAjpIiVQ_8V01TTYCEDUc9ii7bjVkgFAb4ValZGFJZ54PcCnAHvXi5g0ELORzGcTuRqHVAUckMV2otr0g0u_5bWMm6EMAbBrGQCgUGjbZQHjava1Y-5tHXZkPBahJ2LvKRqMmJUlr0anKuJJtJUG03DJYAxABv8YAaXFBnGw6kKJRpUFAC55ry4sp4kGy0NrK2TVWmZW9kStniRv4RaJGI9aZGYwQy2kUykibBNmWEQUlIwIw\";\n    public static final String E = \"AQAB\";\n    public static final String KTY = \"RSA\";\n    public static final String THUMBPRINT = \"HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0\";\n\n    public static final String D_N = \"tP7p9wOe0NWocwLu7h233i1JqUPW1MeLeilyHY7oMKnXZFyf1l0saqLcrBtOj3EyaG6qVfpiLEWEIiuWclPYSR_QSt9lCi9xAoWbYq9-mqseehXPaejynlIMsP2UiCAenSHjJEer6Ug6nFelGVgav3mypwYFUdvc18wI00clKYhRAc4dZodilRzDTLy95V1S3RCxGf-lE0XYg7ieO_ovSMERtH_7NsjZnBiaE7mwm0YZzreCr8oSuHwhC63kgY27FnCgH0h63LICSPVVDJZPLcWAmSXv1k0qoVTsRzFutRN6RB_96wqTTBi8Qm98lyCpXcsxa3BH-4TCvLEaa2KkeQ\";\n    public static final String D_E = \"AQAB\";\n    public static final String D_KTY = \"RSA\";\n    public static final String D_THUMBPRINT = \"0VPbh7-I6swlkBu0TrNKSQp6d69bukzeQA0ksuX3FFs\";\n\n    public static final String ACME_SERVER_URI = \"https://example.com/acme\";\n    public static final String ACCOUNT_URL = \"https://example.com/acme/account/1\";\n\n    public static final String DUMMY_NONCE = Base64.getUrlEncoder().withoutPadding().encodeToString(\"foo-nonce-foo\".getBytes());\n\n    public static final String CERT_ISSUER = \"Pebble Intermediate CA 645fc5\";\n\n    public static final NetworkSettings DEFAULT_NETWORK_SETTINGS = new NetworkSettings();\n\n    private TestUtils() {\n        // utility class without constructor\n    }\n\n    /**\n     * Reads a resource as byte array.\n     *\n     * @param name\n     *            Resource name\n     * @return Resource content as byte array.\n     */\n    public static byte[] getResourceAsByteArray(String name) throws IOException {\n        var buffer = new byte[2048];\n        try (var in = TestUtils.class.getResourceAsStream(name);\n                var out = new ByteArrayOutputStream()) {\n            int len;\n            while ((len = in.read(buffer)) >= 0) {\n                out.write(buffer, 0, len);\n            }\n            return out.toByteArray();\n        }\n    }\n\n    /**\n     * Reads a JSON string from json test files and parses it.\n     *\n     * @param key\n     *            JSON resource\n     * @return Parsed JSON resource\n     */\n    public static JSON getJSON(String key) {\n        try {\n            return JSON.parse(TestUtils.class.getResourceAsStream(\"/json/\" + key + \".json\"));\n        } catch (IOException ex) {\n            throw new UncheckedIOException(ex);\n        }\n    }\n\n    /**\n     * Creates a {@link Session} instance. It uses {@link #ACME_SERVER_URI} as server URI.\n     */\n    public static Session session() {\n        return new Session(URI.create(ACME_SERVER_URI));\n    }\n\n    /**\n     * Creates a {@link Login} instance. It uses {@link #ACME_SERVER_URI} as server URI,\n     * {@link #ACCOUNT_URL} as account URL, and a random key pair.\n     */\n    public static Login login() {\n        try {\n            return session().login(URI.create(ACCOUNT_URL).toURL(), createKeyPair());\n        } catch (IOException ex) {\n            throw new UncheckedIOException(ex);\n        }\n    }\n\n    /**\n     * Creates an {@link URL} from a String. Only throws a runtime exception if the URL is\n     * malformed.\n     *\n     * @param url\n     *            URL to use\n     * @return {@link URL} object\n     */\n    public static URL url(String url) {\n        try {\n            return URI.create(url).toURL();\n        } catch (MalformedURLException ex) {\n            throw new IllegalArgumentException(url, ex);\n        }\n    }\n\n    /**\n     * Creates a {@link Session} instance. It uses {@link #ACME_SERVER_URI} as server URI.\n     *\n     * @param provider\n     *            {@link AcmeProvider} to be used in this session\n     */\n    public static Session session(final AcmeProvider provider) {\n        return new Session(URI.create(ACME_SERVER_URI)) {\n            @Override\n            public AcmeProvider provider() {\n                return provider;\n            }\n\n            @Override\n            public Connection connect() {\n                return provider.connect(getServerUri(), DEFAULT_NETWORK_SETTINGS, getHttpClient());\n            }\n        };\n    }\n\n    /**\n     * Creates a standard account {@link KeyPair} for testing. The key pair is read from a\n     * test resource and is guaranteed not to change between test runs.\n     * <p>\n     * The constants {@link #N}, {@link #E}, {@link #KTY} and {@link #THUMBPRINT} are\n     * related to the returned key pair and can be used for asserting results.\n     *\n     * @return {@link KeyPair} for testing\n     */\n    public static KeyPair createKeyPair() {\n        try {\n            var keyFactory = KeyFactory.getInstance(KTY);\n\n            var publicKeySpec = new X509EncodedKeySpec(getResourceAsByteArray(\"/public.key\"));\n            var publicKey = keyFactory.generatePublic(publicKeySpec);\n\n            var privateKeySpec = new PKCS8EncodedKeySpec(getResourceAsByteArray(\"/private.key\"));\n            var privateKey = keyFactory.generatePrivate(privateKeySpec);\n\n            return new KeyPair(publicKey, privateKey);\n        } catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException ex) {\n            throw new IllegalStateException(ex);\n        }\n    }\n\n    /**\n     * Creates a standard domain key pair for testing. This keypair is read from a test\n     * resource and is guaranteed not to change between test runs.\n     * <p>\n     * The constants {@link #D_N}, {@link #D_E}, {@link #D_KTY} and {@link #D_THUMBPRINT}\n     * are related to the returned key pair and can be used for asserting results.\n     *\n     * @return {@link KeyPair} for testing\n     */\n    public static KeyPair createDomainKeyPair() throws IOException {\n        try {\n            var keyFactory = KeyFactory.getInstance(KTY);\n\n            var publicKeySpec = new X509EncodedKeySpec(getResourceAsByteArray(\"/domain-public.key\"));\n            var publicKey = keyFactory.generatePublic(publicKeySpec);\n\n            var privateKeySpec = new PKCS8EncodedKeySpec(getResourceAsByteArray(\"/domain-private.key\"));\n            var privateKey = keyFactory.generatePrivate(privateKeySpec);\n\n            return new KeyPair(publicKey, privateKey);\n        } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {\n            throw new IOException(ex);\n        }\n    }\n\n    /**\n     * Creates a random ECC key pair with the given curve name.\n     *\n     * @param name\n     *            Curve name\n     * @return {@link KeyPair} for testing\n     */\n    public static KeyPair createECKeyPair(String name) throws IOException {\n        try {\n            var ecSpec = new ECGenParameterSpec(name);\n            var keyGen = KeyPairGenerator.getInstance(\"EC\");\n            keyGen.initialize(ecSpec, new SecureRandom());\n            return keyGen.generateKeyPair();\n        } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException ex) {\n            throw new IOException(ex);\n        }\n    }\n\n    /**\n     * Creates a HMAC key using the given hash algorithm.\n     *\n     * @param algorithm\n     *            Name of the hash algorithm to be used\n     * @return {@link SecretKey} for testing\n     */\n    public static SecretKey createSecretKey(String algorithm) throws IOException {\n        try {\n            var md = MessageDigest.getInstance(algorithm);\n            md.update(\"Turpentine\".getBytes()); // A random password\n            var macKey = md.digest();\n            return new HmacKey(macKey);\n        } catch (NoSuchAlgorithmException ex) {\n            throw new IOException(ex);\n        }\n    }\n\n    /**\n     * Creates a standard certificate chain for testing. This certificate is read from a\n     * test resource and is guaranteed not to change between test runs.\n     *\n     * @param resource\n     *         Name of the resource\n     * @return List of {@link X509Certificate} for testing\n     */\n    public static List<X509Certificate> createCertificate(String resource) throws IOException {\n        try (var in = TestUtils.class.getResourceAsStream(resource)) {\n            var cf = CertificateFactory.getInstance(\"X.509\");\n            return cf.generateCertificates(in).stream()\n                       .map(c -> (X509Certificate) c)\n                       .collect(toUnmodifiableList());\n        } catch (CertificateException ex) {\n            throw new IOException(ex);\n        }\n    }\n\n    /**\n     * Creates a {@link Problem} with the given type and details.\n     *\n     * @param type\n     *            Problem type\n     * @param detail\n     *            Problem details\n     * @param instance\n     *            Instance, or {@code null}\n     * @return Created {@link Problem} object\n     */\n    public static Problem createProblem(URI type, String detail, @Nullable URL instance) {\n        var jb = new JSONBuilder();\n        jb.put(\"type\", type);\n        jb.put(\"detail\", detail);\n        if (instance != null) {\n            jb.put(\"instance\", instance);\n        }\n\n        return new Problem(jb.toJSON(), url(\"https://example.com/acme/1\"));\n    }\n\n    /**\n     * Generates a new keypair for unit tests, and return its N, E, KTY and THUMBPRINT\n     * parameters to be set in the {@link TestUtils} class.\n     */\n    public static void main(String... args) throws Exception {\n        var keyGen = KeyPairGenerator.getInstance(\"RSA\");\n        keyGen.initialize(2048);\n        var keyPair = keyGen.generateKeyPair();\n\n        try (var out = new FileOutputStream(\"public.key\")) {\n            out.write(keyPair.getPublic().getEncoded());\n        }\n\n        try (var out = new FileOutputStream(\"private.key\")) {\n            out.write(keyPair.getPrivate().getEncoded());\n        }\n\n        var jwk = JsonWebKey.Factory.newJwk(keyPair.getPublic());\n        var params = new TreeMap<>(jwk.toParams(OutputControlLevel.PUBLIC_ONLY));\n        var md = MessageDigest.getInstance(\"SHA-256\");\n        md.update(JsonUtil.toJson(params).getBytes(UTF_8));\n        var thumbprint = md.digest();\n\n        System.out.println(\"N = \" + params.get(\"n\"));\n        System.out.println(\"E = \" + params.get(\"e\"));\n        System.out.println(\"KTY = \" + params.get(\"kty\"));\n        System.out.println(\"THUMBPRINT = \" + Base64.getUrlEncoder().withoutPadding().encodeToString(thumbprint));\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/util/CSRBuilderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.util;\n\nimport static org.assertj.core.api.Assertions.*;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.StringReader;\nimport java.io.StringWriter;\nimport java.net.InetAddress;\nimport java.net.UnknownHostException;\nimport java.nio.charset.StandardCharsets;\nimport java.security.KeyPair;\nimport java.security.Security;\nimport java.util.Arrays;\n\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.bouncycastle.asn1.ASN1Encodable;\nimport org.bouncycastle.asn1.ASN1IA5String;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.DEROctetString;\nimport org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;\nimport org.bouncycastle.asn1.x500.X500Name;\nimport org.bouncycastle.asn1.x500.style.BCStyle;\nimport org.bouncycastle.asn1.x509.Extension;\nimport org.bouncycastle.asn1.x509.Extensions;\nimport org.bouncycastle.asn1.x509.GeneralName;\nimport org.bouncycastle.asn1.x509.GeneralNames;\nimport org.bouncycastle.jce.provider.BouncyCastleProvider;\nimport org.bouncycastle.openssl.PEMParser;\nimport org.bouncycastle.pkcs.PKCS10CertificationRequest;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Identifier;\n\n/**\n * Unit tests for {@link CSRBuilder}.\n */\npublic class CSRBuilderTest {\n\n    private static KeyPair testKey;\n    private static KeyPair testEcKey;\n\n    /**\n     * Add provider, create some key pairs\n     */\n    @BeforeAll\n    public static void setup() {\n        Security.addProvider(new BouncyCastleProvider());\n\n        testKey = KeyPairUtils.createKeyPair(512);\n        testEcKey = KeyPairUtils.createECKeyPair(\"secp256r1\");\n    }\n\n    /**\n     * Test if the generated CSR is plausible.\n     */\n    @Test\n    public void testGenerate() throws IOException {\n        var builder = createBuilderWithValues();\n\n        builder.sign(testKey);\n\n        var csr = builder.getCSR();\n        assertThat(csr).isNotNull();\n        assertThat(csr.getEncoded()).isEqualTo(builder.getEncoded());\n\n        csrTest(csr);\n        writerTest(builder);\n    }\n\n    /**\n     * Test if the generated CSR is plausible using a ECDSA key.\n     */\n    @Test\n    public void testECCGenerate() throws IOException {\n        var builder = createBuilderWithValues();\n\n        builder.sign(testEcKey);\n\n        var csr = builder.getCSR();\n        assertThat(csr).isNotNull();\n        assertThat(csr.getEncoded()).isEqualTo(builder.getEncoded());\n\n        csrTest(csr);\n        writerTest(builder);\n    }\n\n    /**\n     * Make sure an exception is thrown when no domain is set.\n     */\n    @Test\n    public void testNoDomain() {\n        var ise = assertThrows(IllegalStateException.class, () -> {\n            var builder = new CSRBuilder();\n            builder.sign(testKey);\n        });\n        assertThat(ise.getMessage())\n            .isEqualTo(\"No domain or IP address was set\");\n    }\n\n    /**\n     * Make sure an exception is thrown when an unknown identifier type is used.\n     */\n    @Test\n    public void testUnknownType() {\n        var iae = assertThrows(IllegalArgumentException.class, () -> {\n            var builder = new CSRBuilder();\n            builder.addIdentifier(new Identifier(\"UnKnOwN\", \"123\"));\n        });\n        assertThat(iae.getMessage())\n            .isEqualTo(\"Unknown identifier type: UnKnOwN\");\n    }\n\n    /**\n     * Make sure all getters will fail if the CSR is not signed.\n     */\n    @Test\n    public void testNoSign() {\n        var builder = new CSRBuilder();\n\n        assertThatIllegalStateException()\n            .isThrownBy(builder::getCSR)\n            .as(\"getCSR()\")\n            .withMessage(\"sign CSR first\");\n\n        assertThatIllegalStateException()\n            .isThrownBy(builder::getEncoded)\n            .as(\"getCSR()\")\n            .withMessage(\"sign CSR first\");\n\n        assertThatIllegalStateException()\n            .isThrownBy(() -> {\n                try (StringWriter w = new StringWriter()) {\n                    builder.write(w);\n                }\n            })\n            .as(\"builder.write()\")\n            .withMessage(\"sign CSR first\");\n    }\n    \n    /**\n     * Checks that addValue behaves correctly in dependence of the\n     * attributes being added. If a common name is set, it should\n     * be handled in the same way when it's added by using\n     * <code>addDomain</code>\n     */\n    @Test\n    public void testAddAttrValues() {\n        var builder = new CSRBuilder();\n        String invAttNameExMessage = assertThrows(IllegalArgumentException.class,\n                () -> X500Name.getDefaultStyle().attrNameToOID(\"UNKNOWNATT\")).getMessage();\n        \n        assertThat(builder.toString()).isEqualTo(\"\");\n\n        assertThatNullPointerException()\n            .isThrownBy(() -> new CSRBuilder().addValue((String) null, \"value\"))\n            .as(\"addValue(String, String)\");\n        assertThatNullPointerException()\n            .isThrownBy(() -> new CSRBuilder().addValue((ASN1ObjectIdentifier) null, \"value\"))\n            .as(\"addValue(ASN1ObjectIdentifier, String)\");\n        assertThatNullPointerException()\n            .isThrownBy(() -> new CSRBuilder().addValue(\"C\", null))\n            .as(\"addValue(String, null)\");\n        assertThatIllegalArgumentException()\n            .isThrownBy(() -> new CSRBuilder().addValue(\"UNKNOWNATT\", \"val\"))\n            .as(\"addValue(String, null)\")\n            .withMessage(invAttNameExMessage);\n        \n        assertThat(builder.toString()).isEqualTo(\"\");\n\n        builder.addValue(\"C\", \"DE\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE\");\n        builder.addValue(\"E\", \"contact@example.com\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,E=contact@example.com\");\n        builder.addValue(\"CN\", \"firstcn.example.com\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,E=contact@example.com,CN=firstcn.example.com,DNS=firstcn.example.com\");\n        builder.addValue(\"CN\", \"scnd.example.com\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,E=contact@example.com,CN=firstcn.example.com,DNS=firstcn.example.com,DNS=scnd.example.com\");\n        \n        builder = new CSRBuilder();\n        builder.addValue(BCStyle.C, \"DE\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE\");\n        builder.addValue(BCStyle.EmailAddress, \"contact@example.com\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,E=contact@example.com\");\n        builder.addValue(BCStyle.CN, \"firstcn.example.com\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,E=contact@example.com,CN=firstcn.example.com,DNS=firstcn.example.com\");\n        builder.addValue(BCStyle.CN, \"scnd.example.com\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,E=contact@example.com,CN=firstcn.example.com,DNS=firstcn.example.com,DNS=scnd.example.com\");\n    }\n\n    private CSRBuilder createBuilderWithValues() throws UnknownHostException {\n        var builder = new CSRBuilder();\n        builder.addDomain(\"abc.de\");\n        builder.addDomain(\"fg.hi\");\n        builder.addDomains(\"jklm.no\", \"pqr.st\");\n        builder.addDomains(Arrays.asList(\"uv.wx\", \"y.z\"));\n        builder.addDomain(\"*.wild.card\");\n        builder.addIP(InetAddress.getByName(\"192.0.2.1\"));\n        builder.addIP(InetAddress.getByName(\"192.0.2.2\"));\n        builder.addIPs(InetAddress.getByName(\"198.51.100.1\"), InetAddress.getByName(\"198.51.100.2\"));\n        builder.addIPs(Arrays.asList(InetAddress.getByName(\"2001:db8::1\"), InetAddress.getByName(\"2001:db8::2\")));\n        builder.addIdentifier(Identifier.dns(\"ide1.nt\"));\n        builder.addIdentifier(Identifier.ip(\"203.0.113.5\"));\n        builder.addIdentifiers(Identifier.dns(\"ide2.nt\"), Identifier.ip(\"203.0.113.6\"));\n        builder.addIdentifiers(Arrays.asList(Identifier.dns(\"ide3.nt\"), Identifier.ip(\"203.0.113.7\")));\n\n        builder.setCommonName(\"abc.de\");\n        builder.setCountry(\"XX\");\n        builder.setLocality(\"Testville\");\n        builder.setOrganization(\"Testing Co\");\n        builder.setOrganizationalUnit(\"Testunit\");\n        builder.setState(\"ABC\");\n\n        assertThat(builder.toString()).isEqualTo(\"CN=abc.de,C=XX,L=Testville,O=Testing Co,\"\n                        + \"OU=Testunit,ST=ABC,\"\n                        + \"DNS=abc.de,DNS=fg.hi,DNS=jklm.no,DNS=pqr.st,DNS=uv.wx,DNS=y.z,DNS=*.wild.card,\"\n                        + \"DNS=ide1.nt,DNS=ide2.nt,DNS=ide3.nt,\"\n                        + \"IP=192.0.2.1,IP=192.0.2.2,IP=198.51.100.1,IP=198.51.100.2,\"\n                        + \"IP=2001:db8:0:0:0:0:0:1,IP=2001:db8:0:0:0:0:0:2,\"\n                        + \"IP=203.0.113.5,IP=203.0.113.6,IP=203.0.113.7\");\n        return builder;\n    }\n\n    /**\n     * Checks if the CSR contains the right parameters.\n     * <p>\n     * This is not supposed to be a Bouncy Castle test. If the\n     * {@link PKCS10CertificationRequest} contains the right parameters, we assume that\n     * Bouncy Castle encodes it properly.\n     */\n    private void csrTest(PKCS10CertificationRequest csr) {\n        var name = csr.getSubject();\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(name.getRDNs(BCStyle.CN)).as(\"CN\")\n                    .extracting(rdn -> rdn.getFirst().getValue().toString())\n                    .contains(\"abc.de\");\n            softly.assertThat(name.getRDNs(BCStyle.C)).as(\"C\")\n                    .extracting(rdn -> rdn.getFirst().getValue().toString())\n                    .contains(\"XX\");\n            softly.assertThat(name.getRDNs(BCStyle.L)).as(\"L\")\n                    .extracting(rdn -> rdn.getFirst().getValue().toString())\n                    .contains(\"Testville\");\n            softly.assertThat(name.getRDNs(BCStyle.O)).as(\"O\")\n                    .extracting(rdn -> rdn.getFirst().getValue().toString())\n                    .contains(\"Testing Co\");\n            softly.assertThat(name.getRDNs(BCStyle.OU)).as(\"OU\")\n                    .extracting(rdn -> rdn.getFirst().getValue().toString())\n                    .contains(\"Testunit\");\n            softly.assertThat(name.getRDNs(BCStyle.ST)).as(\"ST\")\n                    .extracting(rdn -> rdn.getFirst().getValue().toString())\n                    .contains(\"ABC\");\n        }\n\n        var attr = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);\n        assertThat(attr).hasSize(1);\n\n        var extensions = attr[0].getAttrValues().toArray();\n        assertThat(extensions).hasSize(1);\n\n        var names = GeneralNames.fromExtensions((Extensions) extensions[0], Extension.subjectAlternativeName);\n        assertThat(names.getNames())\n                .filteredOn(gn -> gn.getTagNo() == GeneralName.dNSName)\n                .extracting(gn -> ASN1IA5String.getInstance(gn.getName()).getString())\n                .containsExactlyInAnyOrder(\"abc.de\", \"fg.hi\", \"jklm.no\", \"pqr.st\",\n                        \"uv.wx\", \"y.z\", \"*.wild.card\", \"ide1.nt\", \"ide2.nt\", \"ide3.nt\");\n\n        assertThat(names.getNames())\n                .filteredOn(gn -> gn.getTagNo() == GeneralName.iPAddress)\n                .extracting(gn -> getIP(gn.getName()).getHostAddress())\n                .containsExactlyInAnyOrder(\"192.0.2.1\", \"192.0.2.2\", \"198.51.100.1\",\n                        \"198.51.100.2\", \"2001:db8:0:0:0:0:0:1\", \"2001:db8:0:0:0:0:0:2\",\n                        \"203.0.113.5\", \"203.0.113.6\", \"203.0.113.7\");\n    }\n\n    /**\n     * Checks if the {@link CSRBuilder#write(java.io.Writer)} method generates a correct\n     * CSR PEM file.\n     */\n    private void writerTest(CSRBuilder builder) throws IOException {\n        // Write CSR to PEM\n        String pem;\n        try (var out = new StringWriter()) {\n            builder.write(out);\n            pem = out.toString();\n        }\n\n        // Make sure PEM file is properly formatted\n        assertThat(pem).matches(\n                  \"-----BEGIN CERTIFICATE REQUEST-----[\\\\r\\\\n]+\"\n                + \"([a-zA-Z0-9/+=]+[\\\\r\\\\n]+)+\"\n                + \"-----END CERTIFICATE REQUEST-----[\\\\r\\\\n]*\");\n\n        // Read CSR from PEM\n        PKCS10CertificationRequest readCsr;\n        try (var parser = new PEMParser(new StringReader(pem))) {\n            readCsr = (PKCS10CertificationRequest) parser.readObject();\n        }\n\n        // Verify that both keypairs are the same\n        assertThat(builder.getCSR()).isNotSameAs(readCsr);\n        assertThat(builder.getEncoded()).isEqualTo(readCsr.getEncoded());\n\n        // OutputStream is identical?\n        byte[] pemBytes;\n        try (var baos = new ByteArrayOutputStream()) {\n            builder.write(baos);\n            pemBytes = baos.toByteArray();\n        }\n        assertThat(new String(pemBytes, StandardCharsets.UTF_8)).isEqualTo(pem);\n    }\n\n    /**\n     * Fetches the {@link InetAddress} from the given iPAddress record.\n     *\n     * @param name\n     *            Name to convert\n     * @return {@link InetAddress}\n     * @throws IllegalArgumentException\n     *             if the IP address could not be read\n     */\n    private static InetAddress getIP(ASN1Encodable name) {\n        try {\n            return InetAddress.getByAddress(DEROctetString.getInstance(name).getOctets());\n        } catch (UnknownHostException ex) {\n            throw new IllegalArgumentException(ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/util/CertificateUtilsTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.util;\n\nimport static java.time.temporal.ChronoUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.lang.reflect.Modifier;\nimport java.net.InetAddress;\nimport java.net.UnknownHostException;\nimport java.security.KeyPair;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\nimport java.security.cert.CertificateParsingException;\nimport java.security.cert.X509Certificate;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Date;\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport org.bouncycastle.asn1.ASN1InputStream;\nimport org.bouncycastle.asn1.BERTags;\nimport org.bouncycastle.asn1.DEROctetString;\nimport org.bouncycastle.asn1.x509.GeneralName;\nimport org.bouncycastle.pkcs.PKCS10CertificationRequest;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.challenge.TlsAlpn01Challenge;\nimport org.shredzone.acme4j.toolbox.AcmeUtils;\n\n/**\n * Unit tests for {@link CertificateUtils}.\n */\npublic class CertificateUtilsTest {\n\n    /**\n     * Test if {@link CertificateUtils#readCSR(InputStream)} reads an identical CSR.\n     */\n    @Test\n    public void testReadCSR() throws IOException {\n        var keypair = KeyPairUtils.createKeyPair(2048);\n\n        var builder = new CSRBuilder();\n        builder.addDomains(\"example.com\", \"example.org\");\n        builder.sign(keypair);\n\n        var original = builder.getCSR();\n        byte[] pemFile;\n        try (var baos = new ByteArrayOutputStream()) {\n            builder.write(baos);\n            pemFile = baos.toByteArray();\n        }\n\n        try (var bais = new ByteArrayInputStream(pemFile)) {\n            var read = CertificateUtils.readCSR(bais);\n            assertThat(original.getEncoded()).isEqualTo(read.getEncoded());\n        }\n    }\n\n    /**\n     * Test that constructor is private.\n     */\n    @Test\n    public void testPrivateConstructor() throws Exception {\n        var constructor = CertificateUtils.class.getDeclaredConstructor();\n        assertThat(Modifier.isPrivate(constructor.getModifiers())).isTrue();\n        constructor.setAccessible(true);\n        constructor.newInstance();\n    }\n\n    /**\n     * Test if\n     * {@link CertificateUtils#createTlsAlpn01Certificate(KeyPair, Identifier, byte[])}\n     * with domain name creates a good certificate.\n     */\n    @Test\n    public void testCreateTlsAlpn01Certificate() throws Exception {\n        var keypair = KeyPairUtils.createKeyPair(2048);\n        var subject = \"example.com\";\n        var acmeValidationV1 = AcmeUtils.sha256hash(\"rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ\");\n\n        var cert = CertificateUtils.createTlsAlpn01Certificate(keypair, Identifier.dns(subject), acmeValidationV1);\n\n        var now = Instant.now();\n        var end = now.plus(Duration.ofDays(8));\n\n        assertThat(cert).isNotNull();\n        assertThat(cert.getNotAfter()).isAfter(Date.from(now));\n        assertThat(cert.getNotAfter()).isBefore(Date.from(end));\n        assertThat(cert.getNotBefore()).isBeforeOrEqualTo(Date.from(now));\n\n        assertThat(cert.getSubjectX500Principal().getName()).isEqualTo(\"CN=acme.invalid\");\n        assertThat(getSANs(cert)).contains(subject);\n\n        assertThat(cert.getCriticalExtensionOIDs()).contains(TlsAlpn01Challenge.ACME_VALIDATION_OID);\n\n        var encodedExtensionValue = cert.getExtensionValue(TlsAlpn01Challenge.ACME_VALIDATION_OID);\n        assertThat(encodedExtensionValue).isNotNull();\n\n        try (var asn = new ASN1InputStream(new ByteArrayInputStream(encodedExtensionValue))) {\n            var derOctetString = (DEROctetString) asn.readObject();\n\n            var test = new byte[acmeValidationV1.length + 2];\n            test[0] = BERTags.OCTET_STRING;\n            test[1] = (byte) acmeValidationV1.length;\n            System.arraycopy(acmeValidationV1, 0, test, 2, acmeValidationV1.length);\n\n            assertThat(derOctetString.getOctets()).isEqualTo(test);\n        }\n\n        cert.verify(keypair.getPublic());\n    }\n\n    /**\n     * Test if\n     * {@link CertificateUtils#createTlsAlpn01Certificate(KeyPair, Identifier, byte[])}\n     * with IP creates a good certificate.\n     */\n    @Test\n    public void testCreateTlsAlpn01CertificateWithIp() throws IOException, CertificateParsingException {\n        var keypair = KeyPairUtils.createKeyPair(2048);\n        var subject = InetAddress.getLocalHost();\n        var acmeValidationV1 = AcmeUtils.sha256hash(\"rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ\");\n\n        var cert = CertificateUtils.createTlsAlpn01Certificate(keypair, Identifier.ip(subject), acmeValidationV1);\n\n        assertThat(cert.getSubjectX500Principal().getName()).isEqualTo(\"CN=acme.invalid\");\n        assertThat(getIpSANs(cert)).contains(subject);\n    }\n\n    /**\n     * Test if {@link CertificateUtils#createTestRootCertificate(String, Instant, Instant,\n     * KeyPair)} generates a valid root certificate.\n     */\n    @Test\n    public void testCreateTestRootCertificate() throws Exception {\n        var keypair = KeyPairUtils.createKeyPair(2048);\n        var subject = \"CN=Test Root Certificate\";\n        var notBefore = Instant.now().truncatedTo(SECONDS);\n        var notAfter = notBefore.plus(Duration.ofDays(14)).truncatedTo(SECONDS);\n\n        var cert = CertificateUtils.createTestRootCertificate(subject,\n                notBefore, notAfter, keypair);\n\n        assertThat(cert.getIssuerX500Principal().getName()).isEqualTo(subject);\n        assertThat(cert.getSubjectX500Principal().getName()).isEqualTo(subject);\n        assertThat(cert.getNotBefore().toInstant()).isEqualTo(notBefore);\n        assertThat(cert.getNotAfter().toInstant()).isEqualTo(notAfter);\n        assertThat(cert.getSerialNumber()).isNotNull();\n        assertThat(cert.getPublicKey()).isEqualTo(keypair.getPublic());\n        cert.verify(cert.getPublicKey()); // self-signed\n    }\n\n    /**\n     * Test if {@link CertificateUtils#createTestIntermediateCertificate(String, Instant,\n     * Instant, PublicKey, X509Certificate, PrivateKey)} generates a valid intermediate\n     * certificate.\n     */\n    @Test\n    public void testCreateTestIntermediateCertificate() throws Exception {\n        var rootKeypair = KeyPairUtils.createKeyPair(2048);\n        var rootSubject = \"CN=Test Root Certificate\";\n        var rootNotBefore = Instant.now().minus(Duration.ofDays(1)).truncatedTo(SECONDS);\n        var rootNotAfter = rootNotBefore.plus(Duration.ofDays(14)).truncatedTo(SECONDS);\n\n        var rootCert = CertificateUtils.createTestRootCertificate(rootSubject,\n                rootNotBefore, rootNotAfter, rootKeypair);\n\n        var keypair = KeyPairUtils.createKeyPair(2048);\n        var subject = \"CN=Test Intermediate Certificate\";\n        var notBefore = Instant.now().truncatedTo(SECONDS);\n        var notAfter = notBefore.plus(Duration.ofDays(7)).truncatedTo(SECONDS);\n\n        var cert = CertificateUtils.createTestIntermediateCertificate(subject,\n                notBefore, notAfter, keypair.getPublic(), rootCert, rootKeypair.getPrivate());\n\n        assertThat(cert.getIssuerX500Principal().getName()).isEqualTo(rootSubject);\n        assertThat(cert.getSubjectX500Principal().getName()).isEqualTo(subject);\n        assertThat(cert.getNotBefore().toInstant()).isEqualTo(notBefore);\n        assertThat(cert.getNotAfter().toInstant()).isEqualTo(notAfter);\n        assertThat(cert.getSerialNumber()).isNotNull();\n        assertThat(cert.getSerialNumber()).isNotEqualTo(rootCert.getSerialNumber());\n        assertThat(cert.getPublicKey()).isEqualTo(keypair.getPublic());\n        cert.verify(rootKeypair.getPublic()); // signed by root\n    }\n\n    /**\n     * Test if {@link CertificateUtils#createTestCertificate(PKCS10CertificationRequest,\n     * Instant, Instant, X509Certificate, PrivateKey)} generates a valid certificate.\n     */\n    @Test\n    public void testCreateTestCertificate() throws Exception {\n        var rootKeypair = KeyPairUtils.createKeyPair(2048);\n        var rootSubject = \"CN=Test Root Certificate\";\n        var rootNotBefore = Instant.now().minus(Duration.ofDays(1)).truncatedTo(SECONDS);\n        var rootNotAfter = rootNotBefore.plus(Duration.ofDays(14)).truncatedTo(SECONDS);\n\n        var rootCert = CertificateUtils.createTestRootCertificate(rootSubject,\n                rootNotBefore, rootNotAfter, rootKeypair);\n\n        var keypair = KeyPairUtils.createKeyPair(2048);\n        var notBefore = Instant.now().truncatedTo(SECONDS);\n        var notAfter = notBefore.plus(Duration.ofDays(7)).truncatedTo(SECONDS);\n\n        var builder = new CSRBuilder();\n        builder.addDomains(\"example.org\", \"www.example.org\");\n        builder.addIP(InetAddress.getByName(\"192.0.2.1\"));\n        builder.sign(keypair);\n        var csr = builder.getCSR();\n\n        var cert = CertificateUtils.createTestCertificate(csr, notBefore,\n                notAfter, rootCert, rootKeypair.getPrivate());\n\n        assertThat(cert.getIssuerX500Principal().getName()).isEqualTo(rootSubject);\n        assertThat(cert.getSubjectX500Principal().getName()).isEqualTo(\"\");\n        assertThat(getSANs(cert)).contains(\"example.org\", \"www.example.org\");\n        assertThat(getIpSANs(cert)).contains(InetAddress.getByName(\"192.0.2.1\"));\n        assertThat(cert.getNotBefore().toInstant()).isEqualTo(notBefore);\n        assertThat(cert.getNotAfter().toInstant()).isEqualTo(notAfter);\n        assertThat(cert.getSerialNumber()).isNotNull();\n        assertThat(cert.getSerialNumber()).isNotEqualTo(rootCert.getSerialNumber());\n        assertThat(cert.getPublicKey()).isEqualTo(keypair.getPublic());\n        cert.verify(rootKeypair.getPublic()); // signed by root\n    }\n\n    /**\n     * Extracts all DNSName SANs from a certificate.\n     *\n     * @param cert\n     *            {@link X509Certificate}\n     * @return Set of DNSName\n     */\n    private Set<String> getSANs(X509Certificate cert) throws CertificateParsingException {\n        var result = new HashSet<String>();\n\n        for (var list : cert.getSubjectAlternativeNames()) {\n            if (((Number) list.get(0)).intValue() == GeneralName.dNSName) {\n                result.add((String) list.get(1));\n            }\n        }\n\n        return result;\n    }\n\n    /**\n     * Extracts all IPAddress SANs from a certificate.\n     *\n     * @param cert\n     *            {@link X509Certificate}\n     * @return Set of IPAddresses\n     */\n    private Set<InetAddress> getIpSANs(X509Certificate cert) throws CertificateParsingException, UnknownHostException {\n        var result = new HashSet<InetAddress>();\n\n        for (var list : cert.getSubjectAlternativeNames()) {\n            if (((Number) list.get(0)).intValue() == GeneralName.iPAddress) {\n                result.add(InetAddress.getByName(list.get(1).toString()));\n            }\n        }\n\n        return result;\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/java/org/shredzone/acme4j/util/KeyPairUtilsTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.util;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.io.StringReader;\nimport java.io.StringWriter;\nimport java.lang.reflect.Modifier;\nimport java.security.KeyPair;\nimport java.security.Security;\nimport java.security.interfaces.ECPublicKey;\nimport java.security.interfaces.RSAPublicKey;\n\nimport org.bouncycastle.jce.provider.BouncyCastleProvider;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link KeyPairUtils}.\n */\npublic class KeyPairUtilsTest {\n    private static final int KEY_SIZE = 2048;\n    private static final String EC_CURVE = \"secp256r1\";\n\n    @BeforeAll\n    public static void setup() {\n        Security.addProvider(new BouncyCastleProvider());\n    }\n\n    /**\n     * Test that standard keypair generates a secure key pair.\n     */\n    @Test\n    public void testCreateStandardKeyPair() {\n        var pair = KeyPairUtils.createKeyPair();\n        assertThat(pair).isNotNull();\n        assertThat(pair.getPublic()).isInstanceOf(ECPublicKey.class);\n        var pk = (ECPublicKey) pair.getPublic();\n        assertThat(pk.getAlgorithm()).isEqualTo(\"ECDSA\");\n        assertThat(pk.getParams().getCurve().getField().getFieldSize()).isEqualTo(384);\n    }\n\n    /**\n     * Test that RSA keypairs of the correct size are generated.\n     */\n    @Test\n    public void testCreateKeyPair() {\n        var pair = KeyPairUtils.createKeyPair(KEY_SIZE);\n        assertThat(pair).isNotNull();\n        assertThat(pair.getPublic()).isInstanceOf(RSAPublicKey.class);\n\n        var pub = (RSAPublicKey) pair.getPublic();\n        assertThat(pub.getModulus().bitLength()).isEqualTo(KEY_SIZE);\n    }\n\n    /**\n     * Test that reading and writing keypairs work correctly.\n     */\n    @Test\n    public void testWriteAndRead() throws IOException {\n        // Generate a test keypair\n        var pair = KeyPairUtils.createKeyPair(KEY_SIZE);\n\n        // Write keypair to PEM\n        String pem;\n        try (var out = new StringWriter()) {\n            KeyPairUtils.writeKeyPair(pair, out);\n            pem = out.toString();\n        }\n\n        // Make sure PEM file is properly formatted\n        assertThat(pem).matches(\n                  \"-----BEGIN RSA PRIVATE KEY-----[\\\\r\\\\n]+\"\n                + \"([a-zA-Z0-9/+=]+[\\\\r\\\\n]+)+\"\n                + \"-----END RSA PRIVATE KEY-----[\\\\r\\\\n]*\");\n\n        // Read keypair from PEM\n        KeyPair readPair;\n        try (var in = new StringReader(pem)) {\n            readPair = KeyPairUtils.readKeyPair(in);\n        }\n\n        // Verify that both keypairs are the same\n        assertThat(pair).isNotSameAs(readPair);\n        assertThat(pair.getPublic().getEncoded()).isEqualTo(readPair.getPublic().getEncoded());\n        assertThat(pair.getPrivate().getEncoded()).isEqualTo(readPair.getPrivate().getEncoded());\n    }\n\n    /**\n     * Test that ECDSA keypairs are generated.\n     */\n    @Test\n    public void testCreateECCKeyPair() {\n        var pair = KeyPairUtils.createECKeyPair(EC_CURVE);\n        assertThat(pair).isNotNull();\n        assertThat(pair.getPublic()).isInstanceOf(ECPublicKey.class);\n    }\n\n    /**\n     * Test that reading and writing ECDSA keypairs work correctly.\n     */\n    @Test\n    public void testWriteAndReadEC() throws IOException {\n        // Generate a test keypair\n        var pair = KeyPairUtils.createECKeyPair(EC_CURVE);\n\n        // Write keypair to PEM\n        String pem;\n        try (var out = new StringWriter()) {\n            KeyPairUtils.writeKeyPair(pair, out);\n            pem = out.toString();\n        }\n\n        // Make sure PEM file is properly formatted\n        assertThat(pem).matches(\n                  \"-----BEGIN EC PRIVATE KEY-----[\\\\r\\\\n]+\"\n                + \"([a-zA-Z0-9/+=]+[\\\\r\\\\n]+)+\"\n                + \"-----END EC PRIVATE KEY-----[\\\\r\\\\n]*\");\n\n        // Read keypair from PEM\n        KeyPair readPair;\n        try (var in = new StringReader(pem)) {\n            readPair = KeyPairUtils.readKeyPair(in);\n        }\n\n        // Verify that both keypairs are the same\n        assertThat(pair).isNotSameAs(readPair);\n        assertThat(pair.getPublic().getEncoded()).isEqualTo(readPair.getPublic().getEncoded());\n        assertThat(pair.getPrivate().getEncoded()).isEqualTo(readPair.getPrivate().getEncoded());\n\n        // Write Public Key\n        String publicPem;\n        try (var out = new StringWriter()) {\n            KeyPairUtils.writePublicKey(pair.getPublic(), out);\n            publicPem = out.toString();\n        }\n\n        // Make sure PEM file is properly formatted\n        assertThat(publicPem).matches(\n                  \"-----BEGIN PUBLIC KEY-----[\\\\r\\\\n]+\"\n                + \"([a-zA-Z0-9/+=]+[\\\\r\\\\n]+)+\"\n                + \"-----END PUBLIC KEY-----[\\\\r\\\\n]*\");\n    }\n\n    /**\n     * Test that constructor is private.\n     */\n    @Test\n    public void testPrivateConstructor() throws Exception {\n        var constructor = KeyPairUtils.class.getDeclaredConstructor();\n        assertThat(Modifier.isPrivate(constructor.getModifiers())).isTrue();\n        constructor.setAccessible(true);\n        constructor.newInstance();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/.gitignore",
    "content": ""
  },
  {
    "path": "acme4j-client/src/test/resources/META-INF/services/org.shredzone.acme4j.provider.AcmeProvider",
    "content": "# Testing\norg.shredzone.acme4j.connector.SessionProviderTest$Provider1\n\n# Testing2\norg.shredzone.acme4j.connector.SessionProviderTest$Provider2\n"
  },
  {
    "path": "acme4j-client/src/test/resources/ari-example-cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBQzCB66ADAgECAgUAh2VDITAKBggqhkjOPQQDAjAVMRMwEQYDVQQDEwpFeGFt\ncGxlIENBMCIYDzAwMDEwMTAxMDAwMDAwWhgPMDAwMTAxMDEwMDAwMDBaMBYxFDAS\nBgNVBAMTC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeBZu\n7cbpAYNXZLbbh8rNIzuOoqOOtmxA1v7cRm//AwyMwWxyHz4zfwmBhcSrf47NUAFf\nqzLQ2PPQxdTXREYEnKMjMCEwHwYDVR0jBBgwFoAUaYhba4dGQEHhs3uEe6CuLN4B\nyNQwCgYIKoZIzj0EAwIDRwAwRAIge09+S5TZAlw5tgtiVvuERV6cT4mfutXIlwTb\n+FYN/8oCIClDsqBklhB9KAelFiYt9+6FDj3z4KGVelYM5MdsO3pK\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme4j-client/src/test/resources/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDFzCCAf+gAwIBAgIIYZRPVr9ji5UwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE\nAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA2NDVmYzUwHhcNMTcwNDI2MTE0NDEz\nWhcNMjIwNDI2MTE0NDEzWjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBANCRYYYLLZxJeoJKOcSwe+VpwUR/vehv\nx1dMy1fZoK3UX9sDcc5kRKxQJ7vog7q6XG4vA4fGcrGAfG6AeuwplWq3kb3UzYeq\nJeESeoRG0QhWVwCtIUPPVjHaPS19jP1xaE0vsfzCP3gD4l6W9ZhYlIqirFHEFgK8\naKtMxFsmEVR2cDOyH9S5Eoe7QAY43mcflSV6+BzULRwvtT6ds+0Upf0UMbzp0z8V\ndx017MoZdDMAumTaQt8MuIbwxcmRBrZp3pltF3mjGvtBMmuEUoqkiLWtCzhiH2pq\n4T9LDBbilZmjgCWB9pLcqe+KxsdgmBSwPVB/3yhvDaAX0ZuvafjEF68CAwEAAaNX\nMFUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD\nAjAMBgNVHRMBAf8EAjAAMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3\nDQEBCwUAA4IBAQAtZKzESSFF9wVUQdjSe+2P+0OFR7vvfnABs0p1fRv3n17OEgwq\niZEui8aUVkY/mzH90rnL25iIUt+7v4PUUIa7NgZ5adxNvnMvTpuQyFYSwfJODFHZ\nTZnJQJikvmxa0hIoH+zV0s3Pe3OctNeBEMAu2Tq4KsZZY4hF3c7G0Uwe7vmmffgH\ntixADkbOKwqZm1fBzRx6CUjz3u+rmGa4b30unRuF81YI4jqyeOJGNezSYsvLPdIn\np+ISa9mbQvI09bZY/zis0uMGVFcNwKLX3X95xxMONdX7VUsEBq1rFz4ec7priCoi\naEPAD7lAq7FFB1HHwVkPovtYQq7IKXS5VXr4\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDFDCCAfygAwIBAgIIZF/FmWNLATcwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE\nAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA2NDVmYzUwHhcNMTcwNDI2MTE0MzE4\nWhcNNDcwNDI2MTE0MzE4WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRl\nIENBIDY0NWZjNTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMKU292V\nU2wr0+8ZyFGZEEziJrqpNPyWPOZtPhjKQt2H8JeAuAnxBDSUjy8h88NKy/wakzIv\nv+sYWdfpdezg1Ba331KyN31HWX7AMij35cTBQEx+1rzi+9v2S7woGe2UCuSv6cdz\nnJaS0/NOvdDoPSGPctFwOBsCsgx6gr9m5ItanLXMCb8ToKVcUj6GOus0vpB3NNRb\nm8sial/o7Sd4cw52riov1mIkR7Pbi6iACGd/KhFxpKAXQ1UMPTd4tZYGU8pCfyiB\n0rddSmwhh8eWU5ONShLzHi1aDjiu4NEpRxp8K4Tf0MealIJpyjQf5NV8Dz+QG7aX\nDSoH0n+1tGMdMI8CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQWMBQG\nCCsGAQUFBwMBBggrBgEFBQcDAjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB\nCwUAA4IBAQBwOTlS9hOo1RhD2/DgBYZl+gYhqjuBedDJfPS3K34n4Z1FVVG+F6IL\nzP5CGTIABI3L3Ri1pfgUh2lU5nWfE95gUCnmJf8UA0dp0roJInQ25ux/nKFwcuA/\nJL58QZ43TZ/T3BNm8aF/lPvkEut0HnCct1B5IYOzFhqmYS6+BtsiJ2qWxhjiP/yc\nCXq3U289glMeSo7mz6FaUEinx6CZL6qHe5Ins/hMo57Jjay32RHjOeFmx+IlCA0o\n6kXvrZJy1QUpiUkkV7vbnt/PvQLvKo43YR/MsvuYEiOcPoyt7b7FmZ5VXtCnKBcf\n6BcViMAeJ6QzC1qJI6HlWIoqzsO6SKuu\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme4j-client/src/test/resources/certid-cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBQzCB66ADAgECAgUAh2VDITAKBggqhkjOPQQDAjAVMRMwEQYDVQQDEwpFeGFt\ncGxlIENBMCIYDzAwMDEwMTAxMDAwMDAwWhgPMDAwMTAxMDEwMDAwMDBaMBYxFDAS\nBgNVBAMTC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeBZu\n7cbpAYNXZLbbh8rNIzuOoqOOtmxA1v7cRm//AwyMwWxyHz4zfwmBhcSrf47NUAFf\nqzLQ2PPQxdTXREYEnKMjMCEwHwYDVR0jBBgwFoAUaYhba4dGQEHhs3uEe6CuLN4B\nyNQwCgYIKoZIzj0EAwIDRwAwRAIge09+S5TZAlw5tgtiVvuERV6cT4mfutXIlwTb\n+FYN/8oCIClDsqBklhB9KAelFiYt9+6FDj3z4KGVelYM5MdsO3pK\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDSzCCAjOgAwIBAgIIOhNWtJ7Igr0wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVbWluaWNhIHJvb3QgY2EgM2ExMzU2MCAXDTIyMDMxNzE3NTEwOVoYDzIxMjIw\nMzE3MTc1MTA5WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAzYTEzNTYwggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDc3P6cxcCZ7FQOQrYuigReSa8T\nIOPNKmlmX9OrTkPwjThiMNEETYKO1ea99yXPK36LUHC6OLmZ9jVQW2Ny1qwQCOy6\nTrquhnwKgtkBMDAZBLySSEXYdKL3r0jA4sflW130/OLwhstU/yv0J8+pj7eSVOR3\nzJBnYd1AqnXHRSwQm299KXgqema7uwsa8cgjrXsBzAhrwrvYlVhpWFSv3lQRDFQg\nc5Z/ZDV9i26qiaJsCCmdisJZWN7N2luUgxdRqzZ4Cr2Xoilg3T+hkb2y/d6ttsPA\nkaSA+pq3q6Qa7/qfGdT5WuUkcHpvKNRWqnwT9rCYlmG00r3hGgc42D/z1VvfAgMB\nAAGjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr\nBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQ4zzDRUaXHVKql\nSTWkULGU4zGZpTAfBgNVHSMEGDAWgBQ4zzDRUaXHVKqlSTWkULGU4zGZpTANBgkq\nhkiG9w0BAQsFAAOCAQEArbDHhEjGedjb/YjU80aFTPWOMRjgyfQaPPgyxwX6Dsid\n1i2H1x4ud4ntz3sTZZxdQIrOqtlIWTWVCjpStwGxaC+38SdreiTTwy/nikXGa/6W\nZyQRppR3agh/pl5LHVO6GsJz3YHa7wQhEhj3xsRwa9VrRXgHbLGbPOFVRTHPjaPg\nGtsv2PN3f67DsPHF47ASqyOIRpLZPQmZIw6D3isJwfl+8CzvlB1veO0Q3uh08IJc\nfspYQXvFBzYa64uKxNAJMi4Pby8cf4r36Wnb7cL4ho3fOHgAltxdW8jgibRzqZpQ\nQKyxn2jX7kxeUDt0hFDJE8lOrhP73m66eBNzxe//FQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/authorizationChallenges.json",
    "content": "{\n  \"challenges\": [\n    {\n      \"type\": \"http-01\",\n      \"url\": \"https://example.com/authz/asdf/0\",\n      \"token\": \"IlirfxKKXAsHtmzK29Pj8A\"\n    },\n    {\n      \"type\": \"dns-01\",\n      \"url\": \"https://example.com/authz/asdf/1\",\n      \"token\": \"DGyRejmCefe7v4NfDGDKfA\"\n    },\n    {\n      \"type\": \"tls-alpn-01\",\n      \"url\": \"https://example.com/authz/asdf/1\",\n      \"token\": \"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA\"\n    },\n    {\n      \"type\": \"duplicate-01\",\n      \"url\": \"https://example.com/authz/asdf/3\"\n    },\n    {\n      \"type\": \"duplicate-01\",\n      \"url\": \"https://example.com/authz/asdf/4\"\n    }\n  ]\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/canceledOrderResponse.json",
    "content": "{\n  \"status\": \"canceled\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/datatypes.json",
    "content": "{\n  \"text\": \"lorem ipsum\",\n  \"number\": 123,\n  \"boolean\": true,\n  \"uri\": \"mailto:foo@example.com\",\n  \"url\": \"http://example.com\",\n  \"date\": \"2016-01-08T00:00:00Z\",\n  \"array\": [\"foo\", 987, [1, 2, 3], {\"test\": \"ok\"}],\n  \"collect\": [\"foo\", \"bar\", \"barfoo\"],\n  \"status\": \"VALID\",\n  \"binary\": \"Q2hhaW5zYXc\",\n  \"duration\": 86400,\n  \"problem\": {\n    \"type\": \"urn:ietf:params:acme:error:rateLimited\",\n    \"detail\": \"too many requests\",\n    \"instance\": \"/documents/errors.html\"\n  },\n  \"encoded\": \"eyJrZXkiOiJ2YWx1ZSJ9\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/deactivateAccountResponse.json",
    "content": "{\n  \"status\": \"deactivated\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/directory.json",
    "content": "{\n  \"newNonce\": \"https://example.com/acme/new-nonce\",\n  \"newAccount\": \"https://example.com/acme/new-account\",\n  \"newOrder\": \"https://example.com/acme/new-order\",\n  \"newAuthz\": \"https://example.com/acme/new-authz\",\n  \"renewalInfo\": \"https://example.com/acme/renewal-info\",\n  \"meta\": {\n    \"termsOfService\": \"https://example.com/acme/terms\",\n    \"website\": \"https://www.example.com/\",\n    \"caaIdentities\": [\n      \"example.com\"\n    ],\n    \"auto-renewal\": {\n      \"min-lifetime\": 86400,\n      \"max-duration\":  31536000,\n      \"allow-certificate-get\": true\n    },\n    \"externalAccountRequired\": true,\n    \"subdomainAuthAllowed\": true,\n    \"xTestString\": \"foobar\",\n    \"xTestUri\": \"https://www.example.org\",\n    \"xTestArray\": [\n      \"foo\",\n      \"bar\",\n      \"barfoo\"\n    ],\n    \"profiles\": {\n      \"classic\": \"The profile you're accustomed to\",\n      \"custom\": \"Some other profile\"\n    }\n  }\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/directoryNoMeta.json",
    "content": "{\n  \"newAccount\": \"https://example.com/acme/new-account\",\n  \"newAuthz\": \"https://example.com/acme/new-authz\",\n  \"newOrder\": \"https://example.com/acme/new-order\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/dns01Challenge.json",
    "content": "{\n  \"type\": \"dns-01\",\n  \"url\": \"https://example.com/acme/authz/0\",\n  \"status\": \"pending\",\n  \"token\": \"pNvmJivs0WCko2suV7fhe-59oFqyYx_yB7tx6kIMAyE\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/dnsAccount01Challenge.json",
    "content": "{\n  \"type\": \"dns-account-01\",\n  \"url\": \"https://example.com/acme/chall/i00MGYwLWIx\",\n  \"status\": \"pending\",\n  \"token\": \"ODE4OWY4NTktYjhmYS00YmY1LTk5MDgtZTFjYTZmNjZlYTUx\"\n}"
  },
  {
    "path": "acme4j-client/src/test/resources/json/dnsPersist01Challenge.json",
    "content": "{\n  \"type\": \"dns-persist-01\",\n  \"url\": \"https://ca.example/acme/authz/1234/0\",\n  \"status\": \"pending\",\n  \"accounturi\": \"https://example.com/acme/account/1\",\n  \"issuer-domain-names\": [\"authority.example\", \"ca.example.net\"]\n}"
  },
  {
    "path": "acme4j-client/src/test/resources/json/finalizeAutoRenewResponse.json",
    "content": "{\n  \"status\": \"valid\",\n  \"expires\": \"2015-03-01T14:09:00Z\",\n  \"identifiers\": [\n    {\n      \"type\": \"dns\",\n      \"value\": \"example.com\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"www.example.com\"\n    }\n  ],\n  \"auto-renewal\": {\n    \"start-date\": \"2018-01-01T00:00:00Z\",\n    \"end-date\": \"2019-01-01T00:00:00Z\",\n    \"lifetime\": 604800,\n    \"lifetime-adjust\": 518400,\n    \"allow-certificate-get\": true\n  },\n  \"authorizations\": [\n    \"https://example.com/acme/authz/1234\",\n    \"https://example.com/acme/authz/2345\"\n  ],\n  \"finalize\": \"https://example.com/acme/acct/1/order/1/finalize\",\n  \"star-certificate\": \"https://example.com/acme/cert/1234\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/finalizeRequest.json",
    "content": "{\n  \"csr\": \"MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb\"\n}"
  },
  {
    "path": "acme4j-client/src/test/resources/json/finalizeResponse.json",
    "content": "{\n  \"status\": \"valid\",\n  \"expires\": \"2015-03-01T14:09:00Z\",\n  \"identifiers\": [\n    {\n      \"type\": \"dns\",\n      \"value\": \"example.com\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"www.example.com\"\n    }\n  ],\n  \"notBefore\": \"2016-01-01T00:00:00Z\",\n  \"notAfter\": \"2016-01-08T00:00:00Z\",\n  \"authorizations\": [\n    \"https://example.com/acme/authz/1234\",\n    \"https://example.com/acme/authz/2345\"\n  ],\n  \"finalize\": \"https://example.com/acme/acct/1/order/1/finalize\",\n  \"certificate\": \"https://example.com/acme/cert/1234\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/genericChallenge.json",
    "content": "{\n  \"type\": \"generic-01\",\n  \"status\": \"invalid\",\n  \"url\": \"http://example.com/challenge/123\",\n  \"validated\": \"2015-12-12T17:19:36.336785823Z\",\n  \"error\": {\n    \"type\": \"urn:ietf:params:acme:error:incorrectResponse\",\n    \"detail\": \"bad token\",\n    \"instance\": \"/documents/faq.html\"\n  }\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/httpChallenge.json",
    "content": "{\n  \"type\": \"http-01\",\n  \"url\": \"https://example.com/acme/authz/0\",\n  \"status\": \"pending\",\n  \"token\": \"rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/httpNoTokenChallenge.json",
    "content": "{\n  \"type\": \"http-01\",\n  \"url\": \"https://example.com/acme/authz/0\",\n  \"status\": \"pending\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/modifyAccount.json",
    "content": "{\n  \"contact\": [\n    \"mailto:foo@example.com\",\n    \"mailto:foo2@example.com\",\n    \"mailto:foo3@example.com\"\n  ]\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/modifyAccountResponse.json",
    "content": "{\n  \"termsOfServiceAgreed\": true,\n  \"contact\": [\n    \"mailto:foo@example.com\",\n    \"mailto:foo2@example.com\",\n    \"mailto:foo3@example.com\"\n  ]\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/newAccount.json",
    "content": "{\n  \"termsOfServiceAgreed\": true,\n  \"contact\": [\n    \"mailto:foo@example.com\"\n  ]\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/newAccountOnlyExisting.json",
    "content": "{\n  \"onlyReturnExisting\": true\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/newAccountResponse.json",
    "content": "{\n  \"status\": \"valid\",\n  \"termsOfServiceAgreed\": true,\n  \"contact\": [\n    \"mailto:foo@example.com\"\n  ],\n  \"orders\": \"https://example.com/acme/orders/rzGoeA\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/newAuthorizationRequest.json",
    "content": "{\n  \"identifier\": {\n    \"type\": \"dns\",\n    \"value\": \"example.org\"\n  }\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/newAuthorizationRequestSub.json",
    "content": "{\n  \"identifier\": {\n    \"type\": \"dns\",\n    \"value\": \"example.org\",\n    \"subdomainAuthAllowed\": true\n  }\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/newAuthorizationResponse.json",
    "content": "{\n  \"status\": \"pending\",\n  \"identifier\": {\n    \"type\": \"dns\",\n    \"value\": \"example.org\"\n  },\n  \"challenges\": [\n    {\n      \"type\": \"http-01\",\n      \"status\": \"pending\",\n      \"url\": \"https://example.com/authz/asdf/0\",\n      \"token\": \"IlirfxKKXAsHtmzK29Pj8A\"\n    },\n    {\n      \"type\": \"dns-01\",\n      \"status\": \"pending\",\n      \"url\": \"https://example.com/authz/asdf/1\",\n      \"token\": \"DGyRejmCefe7v4NfDGDKfA\"\n    }\n  ]\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/newAuthorizationResponseSub.json",
    "content": "{\n  \"status\": \"pending\",\n  \"identifier\": {\n    \"type\": \"dns\",\n    \"value\": \"example.org\"\n  },\n  \"challenges\": [\n    {\n      \"type\": \"dns-01\",\n      \"status\": \"pending\",\n      \"url\": \"https://example.com/authz/asdf/1\",\n      \"token\": \"DGyRejmCefe7v4NfDGDKfA\"\n    }\n  ],\n  \"subdomainAuthAllowed\": true\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/problem.json",
    "content": "{\n  \"type\": \"urn:ietf:params:acme:error:malformed\",\n  \"title\": \"Some of the identifiers requested were rejected\",\n  \"detail\": \"Identifier \\\"abc12_\\\" is malformed\",\n  \"instance\": \"/documents/error.html\",\n  \"subproblems\": [\n    {\n      \"type\": \"urn:ietf:params:acme:error:malformed\",\n      \"detail\": \"Invalid underscore in DNS name \\\"_example.com\\\"\",\n      \"identifier\": {\n        \"type\": \"dns\",\n        \"value\": \"_example.com\"\n      }\n    },\n    {\n      \"type\": \"urn:ietf:params:acme:error:rejectedIdentifier\",\n      \"detail\": \"This CA will not issue for \\\"example.net\\\"\",\n      \"identifier\": {\n        \"type\": \"dns\",\n        \"value\": \"example.net\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/renewalInfo.json",
    "content": "{\n  \"suggestedWindow\": {\n    \"start\": \"2021-01-03T00:00:00Z\",\n    \"end\": \"2021-01-07T00:00:00Z\"\n  },\n  \"explanationURL\": \"https://example.com/docs/example-mass-reissuance-event\"\n}"
  },
  {
    "path": "acme4j-client/src/test/resources/json/replacedCertificateRequest.json",
    "content": "{\n  \"certID\": \"MFswCwYJYIZIAWUDBAIBBCCeWLRusNLb--vmWOkxm34qDjTMWkc3utIhOMoMwKDqbgQg2iiKWySZrD-6c88HMZ6vhIHZPamChLlzGHeZ7pTS8jYCCD6jRWhlRB8c\",\n  \"replaced\": true\n}"
  },
  {
    "path": "acme4j-client/src/test/resources/json/requestAutoRenewOrderRequest.json",
    "content": "{\n  \"identifiers\": [\n    {\n      \"type\": \"dns\",\n      \"value\": \"example.org\"\n    }\n  ],\n  \"auto-renewal\": {\n    \"start-date\": \"2018-01-01T00:00:00Z\",\n    \"end-date\": \"2019-01-01T00:00:00Z\",\n    \"lifetime\": 604800,\n    \"lifetime-adjust\": 518400,\n    \"allow-certificate-get\": true\n  }\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/requestAutoRenewOrderResponse.json",
    "content": "{\n  \"status\": \"pending\",\n  \"expires\": \"2016-01-10T00:00:00Z\",\n  \"identifiers\": [\n    {\n      \"type\": \"dns\",\n      \"value\": \"example.org\"\n    }\n  ],\n  \"auto-renewal\": {\n    \"start-date\": \"2018-01-01T00:00:00Z\",\n    \"end-date\": \"2019-01-01T00:00:00Z\",\n    \"lifetime\": 604800,\n    \"lifetime-adjust\": 518400,\n    \"allow-certificate-get\": true\n  },\n  \"authorizations\": [\n    \"https://example.com/acme/authz/1234\",\n    \"https://example.com/acme/authz/2345\"\n  ],\n  \"finalize\": \"https://example.com/acme/acct/1/order/1/finalize\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/requestCertificateRequest.json",
    "content": "{\n  \"csr\": \"MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/requestCertificateRequestWithDate.json",
    "content": "{\n  \"csr\": \"MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb\",\n  \"notBefore\": \"2016-01-01T00:00:00Z\",\n  \"notAfter\": \"2016-01-08T00:00:00Z\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/requestOrderRequest.json",
    "content": "{\n  \"identifiers\": [\n    {\n      \"type\": \"dns\",\n      \"value\": \"example.com\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"www.example.com\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"example.org\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"m.example.com\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"m.example.org\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"d.example.com\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"d2.example.com\"\n    },\n    {\n      \"type\": \"ip\",\n      \"value\": \"192.0.2.2\"\n    }\n  ],\n  \"notBefore\": \"2016-01-01T00:00:00Z\",\n  \"notAfter\": \"2016-01-08T00:00:00Z\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/requestOrderRequestSub.json",
    "content": "{\n  \"identifiers\": [\n    {\n      \"type\": \"dns\",\n      \"value\": \"foo.bar.example.com\",\n      \"ancestorDomain\": \"example.com\"\n    },\n  ],\n  \"notBefore\": \"2016-01-01T00:00:00Z\",\n  \"notAfter\": \"2016-01-08T00:00:00Z\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/requestOrderResponse.json",
    "content": "{\n  \"status\": \"pending\",\n  \"expires\": \"2016-01-10T00:00:00Z\",\n  \"identifiers\": [\n    {\n      \"type\": \"dns\",\n      \"value\": \"example.com\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"www.example.com\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"example.org\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"m.example.com\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"m.example.org\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"d.example.com\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"d2.example.com\"\n    },\n    {\n      \"type\": \"ip\",\n      \"value\": \"192.0.2.2\"\n    }\n  ],\n  \"notBefore\": \"2016-01-01T00:10:00Z\",\n  \"notAfter\": \"2016-01-08T00:10:00Z\",\n  \"authorizations\": [\n    \"https://example.com/acme/authz/1234\",\n    \"https://example.com/acme/authz/2345\"\n  ],\n  \"finalize\": \"https://example.com/acme/acct/1/order/1/finalize\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/requestOrderResponseSub.json",
    "content": "{\n  \"status\": \"pending\",\n  \"expires\": \"2016-01-10T00:00:00Z\",\n  \"identifiers\": [\n    {\n      \"type\": \"dns\",\n      \"value\": \"foo.bar.example.com\"\n    }\n  ],\n  \"notBefore\": \"2016-01-01T00:10:00Z\",\n  \"notAfter\": \"2016-01-08T00:10:00Z\",\n  \"authorizations\": [\n    \"https://example.com/acme/authz/1234\",\n    \"https://example.com/acme/authz/2345\"\n  ],\n  \"finalize\": \"https://example.com/acme/acct/1/order/1/finalize\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/requestProfileOrderRequest.json",
    "content": "{\n  \"identifiers\": [\n    {\n      \"type\": \"dns\",\n      \"value\": \"example.org\"\n    }\n  ],\n  \"profile\": \"classic\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/requestProfileOrderResponse.json",
    "content": "{\n  \"status\": \"pending\",\n  \"expires\": \"2016-01-10T00:00:00Z\",\n  \"identifiers\": [\n    {\n      \"type\": \"dns\",\n      \"value\": \"example.org\"\n    }\n  ],\n  \"authorizations\": [\n    \"https://example.com/acme/authz/1234\",\n    \"https://example.com/acme/authz/2345\"\n  ],\n  \"finalize\": \"https://example.com/acme/acct/1/order/1/finalize\",\n  \"profile\": \"classic\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/requestReplacesRequest.json",
    "content": "{\n  \"identifiers\": [\n    {\n      \"type\": \"dns\",\n      \"value\": \"example.org\"\n    }\n  ],\n  \"replaces\": \"aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/requestReplacesResponse.json",
    "content": "{\n  \"status\": \"pending\",\n  \"expires\": \"2016-01-10T00:00:00Z\",\n  \"identifiers\": [\n    {\n      \"type\": \"dns\",\n      \"value\": \"example.org\"\n    }\n  ],\n  \"authorizations\": [\n    \"https://example.com/acme/authz/1234\",\n    \"https://example.com/acme/authz/2345\"\n  ],\n  \"finalize\": \"https://example.com/acme/acct/1/order/1/finalize\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/revokeCertificateRequest.json",
    "content": "{\n  \"certificate\": \"MIIDFzCCAf-gAwIBAgIIYZRPVr9ji5UwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA2NDVmYzUwHhcNMTcwNDI2MTE0NDEzWhcNMjIwNDI2MTE0NDEzWjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANCRYYYLLZxJeoJKOcSwe-VpwUR_vehvx1dMy1fZoK3UX9sDcc5kRKxQJ7vog7q6XG4vA4fGcrGAfG6AeuwplWq3kb3UzYeqJeESeoRG0QhWVwCtIUPPVjHaPS19jP1xaE0vsfzCP3gD4l6W9ZhYlIqirFHEFgK8aKtMxFsmEVR2cDOyH9S5Eoe7QAY43mcflSV6-BzULRwvtT6ds-0Upf0UMbzp0z8Vdx017MoZdDMAumTaQt8MuIbwxcmRBrZp3pltF3mjGvtBMmuEUoqkiLWtCzhiH2pq4T9LDBbilZmjgCWB9pLcqe-KxsdgmBSwPVB_3yhvDaAX0ZuvafjEF68CAwEAAaNXMFUwDgYDVR0PAQH_BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQAtZKzESSFF9wVUQdjSe-2P-0OFR7vvfnABs0p1fRv3n17OEgwqiZEui8aUVkY_mzH90rnL25iIUt-7v4PUUIa7NgZ5adxNvnMvTpuQyFYSwfJODFHZTZnJQJikvmxa0hIoH-zV0s3Pe3OctNeBEMAu2Tq4KsZZY4hF3c7G0Uwe7vmmffgHtixADkbOKwqZm1fBzRx6CUjz3u-rmGa4b30unRuF81YI4jqyeOJGNezSYsvLPdInp-ISa9mbQvI09bZY_zis0uMGVFcNwKLX3X95xxMONdX7VUsEBq1rFz4ec7priCoiaEPAD7lAq7FFB1HHwVkPovtYQq7IKXS5VXr4\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/revokeCertificateWithReasonRequest.json",
    "content": "{\n  \"certificate\": \"MIIDFzCCAf-gAwIBAgIIYZRPVr9ji5UwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA2NDVmYzUwHhcNMTcwNDI2MTE0NDEzWhcNMjIwNDI2MTE0NDEzWjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANCRYYYLLZxJeoJKOcSwe-VpwUR_vehvx1dMy1fZoK3UX9sDcc5kRKxQJ7vog7q6XG4vA4fGcrGAfG6AeuwplWq3kb3UzYeqJeESeoRG0QhWVwCtIUPPVjHaPS19jP1xaE0vsfzCP3gD4l6W9ZhYlIqirFHEFgK8aKtMxFsmEVR2cDOyH9S5Eoe7QAY43mcflSV6-BzULRwvtT6ds-0Upf0UMbzp0z8Vdx017MoZdDMAumTaQt8MuIbwxcmRBrZp3pltF3mjGvtBMmuEUoqkiLWtCzhiH2pq4T9LDBbilZmjgCWB9pLcqe-KxsdgmBSwPVB_3yhvDaAX0ZuvafjEF68CAwEAAaNXMFUwDgYDVR0PAQH_BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQAtZKzESSFF9wVUQdjSe-2P-0OFR7vvfnABs0p1fRv3n17OEgwqiZEui8aUVkY_mzH90rnL25iIUt-7v4PUUIa7NgZ5adxNvnMvTpuQyFYSwfJODFHZTZnJQJikvmxa0hIoH-zV0s3Pe3OctNeBEMAu2Tq4KsZZY4hF3c7G0Uwe7vmmffgHtixADkbOKwqZm1fBzRx6CUjz3u-rmGa4b30unRuF81YI4jqyeOJGNezSYsvLPdInp-ISa9mbQvI09bZY_zis0uMGVFcNwKLX3X95xxMONdX7VUsEBq1rFz4ec7priCoiaEPAD7lAq7FFB1HHwVkPovtYQq7IKXS5VXr4\",\n  \"reason\": 1\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/tlsAlpnChallenge.json",
    "content": "{\n  \"type\": \"tls-alpn-01\",\n  \"url\": \"https://example.com/acme/authz/0\",\n  \"status\": \"pending\",\n  \"token\": \"rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/triggerHttpChallenge.json",
    "content": "{\n  \"type\": \"http-01\",\n  \"status\": \"pending\",\n  \"url\": \"https://example.com/acme/some-location\",\n  \"token\": \"IlirfxKKXAsHtmzK29Pj8A\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/triggerHttpChallengeRequest.json",
    "content": "{\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/triggerHttpChallengeResponse.json",
    "content": "{\n  \"type\": \"http-01\",\n  \"status\": \"pending\",\n  \"url\": \"https://example.com/acme/some-location\",\n  \"token\": \"IlirfxKKXAsHtmzK29Pj8A\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/updateAccount.json",
    "content": "{}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/updateAccountResponse.json",
    "content": "{\n  \"status\": \"valid\",\n  \"contact\": [\n    \"mailto:foo2@example.com\"\n  ],\n  \"termsOfServiceAgreed\": true,\n  \"orders\": \"https://example.com/acme/acct/1/orders\",\n  \"externalAccountBinding\": {\n    \"protected\": \"eyJ1cmwiOiJodHRwOi8vZXhhbXBsZS5jb20vYWNtZS9yZXNvdXJjZSIsImtpZCI6Ik5DQy0xNzAxIiwiYWxnIjoiSFMyNTYifQ\",\n    \"payload\": \"eyJrdHkiOiJSU0EiLCJuIjoicFpzVEtZNDF5X0N3Z0owVlg3Qm1tR3NfN1Vwcm1YUU1HUGNuU2JCZUpBalpIQTlTeXlKS2FXdjRmTlVkQklBWDNZMlFvWml4ajUwblFMeUx2Mm5nM3B2RW9STDBzeDlaSGdwNW5kQWpwSWlWUV84VjAxVFRZQ0VEVWM5aWk3YmpWa2dGQWI0VmFsWkdGSlo1NFBjQ25BSHZYaTVnMEVMT1J6R2NUdVJxSFZBVWNrTVYyb3RyMGcwdV81YldNbTZFTUFiQnJHUUNnVUdqYlpRSGphdmExWS01dEhYWmtQQmFoSjJMdktScU1tSlVscjBhbkt1Skp0SlVHMDNESllBeEFCdjhZQWFYRkJuR3c2a0tKUnBVRkFDNTVyeTRzcDRrR3kwTnJLMlRWV21aVzlrU3RuaVJ2NFJhSkdJOWFaR1l3UXkya1V5a2liQk5tV0VRVWxJd0l3IiwiZSI6IkFRQUIifQ\",\n    \"signature\": \"skPdpjTgx8zIGsNRtvv4zNlfp-uidFDgCMY3Z3ONLgw\"\n  }\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/updateAuthorizationResponse.json",
    "content": "{\n  \"status\": \"valid\",\n  \"expires\": \"2016-01-02T17:12:40Z\",\n  \"identifier\": {\n    \"type\": \"dns\",\n    \"value\": \"example.org\"\n  },\n  \"challenges\": [\n    {\n      \"type\": \"http-01\",\n      \"status\": \"pending\",\n      \"url\": \"https://example.com/authz/asdf/0\",\n      \"token\": \"IlirfxKKXAsHtmzK29Pj8A\"\n    },\n    {\n      \"type\": \"dns-01\",\n      \"status\": \"pending\",\n      \"url\": \"https://example.com/authz/asdf/1\",\n      \"token\": \"DGyRejmCefe7v4NfDGDKfA\"\n    },\n    {\n      \"type\": \"tls-alpn-01\",\n      \"status\": \"pending\",\n      \"url\": \"https://example.com/authz/asdf/2\",\n      \"token\": \"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA\"\n   }\n  ]\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/updateAuthorizationWildcardResponse.json",
    "content": "{\n  \"status\": \"valid\",\n  \"expires\": \"2016-01-02T17:12:40Z\",\n  \"wildcard\": true,\n  \"identifier\": {\n    \"type\": \"dns\",\n    \"value\": \"example.org\"\n  },\n  \"challenges\": [\n    {\n      \"type\": \"dns-01\",\n      \"status\": \"pending\",\n      \"url\": \"https://example.com/authz/asdf/1\",\n      \"token\": \"DGyRejmCefe7v4NfDGDKfA\"\n    }\n  ]\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/updateAutoRenewOrderResponse.json",
    "content": "{\n  \"status\": \"valid\",\n  \"auto-renewal\": {\n    \"start-date\": \"2016-01-01T00:00:00Z\",\n    \"end-date\": \"2017-01-01T00:00:00Z\",\n    \"lifetime\": 604800,\n    \"lifetime-adjust\": 518400,\n    \"allow-certificate-get\": true\n  }\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/updateHttpChallengeResponse.json",
    "content": "{\n  \"type\": \"http-01\",\n  \"url\": \"https://example.com/acme/some-location\",\n  \"status\": \"valid\",\n  \"token\": \"IlirfxKKXAsHtmzK29Pj8A\",\n  \"keyAuthorization\": \"XbmEGDDc2AMDArHLt5x7GxZfIRv0aScknUKlyf5S4KU.KMH_h8aGAKlY3VQqBUczm1cfo9kaovivy59rSY1xZ0E\"\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/json/updateOrderResponse.json",
    "content": "{\n  \"status\": \"pending\",\n  \"expires\": \"2015-03-01T14:09:00Z\",\n  \"identifiers\": [\n    {\n      \"type\": \"dns\",\n      \"value\": \"example.com\"\n    },\n    {\n      \"type\": \"dns\",\n      \"value\": \"www.example.com\"\n    }\n  ],\n  \"notBefore\": \"2016-01-01T00:00:00Z\",\n  \"notAfter\": \"2016-01-08T00:00:00Z\",\n  \"authorizations\": [\n    \"https://example.com/acme/authz/1234\",\n    \"https://example.com/acme/authz/2345\"\n  ],\n  \"finalize\": \"https://example.com/acme/acct/1/order/1/finalize\",\n  \"certificate\": \"https://example.com/acme/cert/1234\",\n  \"error\": {\n    \"type\": \"urn:ietf:params:acme:error:connection\",\n    \"detail\": \"connection refused\"\n  }\n}\n"
  },
  {
    "path": "acme4j-client/src/test/resources/simplelogger.properties",
    "content": "\norg.slf4j.simpleLogger.log.org.shredzone.acme4j = debug\n"
  },
  {
    "path": "acme4j-example/.gitignore",
    "content": "*.key\n*.crt\n*.csr\n"
  },
  {
    "path": "acme4j-example/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n *\n * acme4j - ACME Java client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n *\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.shredzone.acme4j</groupId>\n        <artifactId>acme4j</artifactId>\n        <version>5.1.1-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>acme4j-example</artifactId>\n\n    <name>acme4j Example</name>\n    <description>Example for using acme4j</description>\n\n    <properties>\n        <!-- I prefer readability over maintainability in the examples... -->\n        <spotbugs.skip>true</spotbugs.skip>\n    </properties>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>exec-maven-plugin</artifactId>\n                <version>3.0.0</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>java</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <mainClass>org.shredzone.acme4j.example.ClientTest</mainClass>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.shredzone.acme4j</groupId>\n            <artifactId>acme4j-client</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-simple</artifactId>\n            <version>${slf4j.version}</version>\n            <scope>runtime</scope>\n        </dependency>\n    </dependencies>\n\n</project>\n"
  },
  {
    "path": "acme4j-example/src/main/java/.gitignore",
    "content": ""
  },
  {
    "path": "acme4j-example/src/main/java/module-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\nmodule org.shredzone.acme4j.example {\n    requires org.shredzone.acme4j;\n\n    requires java.desktop;\n\n    requires org.bouncycastle.provider;\n    requires org.slf4j;\n}\n"
  },
  {
    "path": "acme4j-example/src/main/java/org/shredzone/acme4j/example/ClientTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.example;\n\nimport java.io.File;\nimport java.io.FileReader;\nimport java.io.FileWriter;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.security.KeyPair;\nimport java.security.Security;\nimport java.time.Duration;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Optional;\nimport java.util.function.Supplier;\n\nimport javax.swing.JOptionPane;\n\nimport org.bouncycastle.jce.provider.BouncyCastleProvider;\nimport org.shredzone.acme4j.Account;\nimport org.shredzone.acme4j.AccountBuilder;\nimport org.shredzone.acme4j.Authorization;\nimport org.shredzone.acme4j.Certificate;\nimport org.shredzone.acme4j.Order;\nimport org.shredzone.acme4j.Problem;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.challenge.Challenge;\nimport org.shredzone.acme4j.challenge.Dns01Challenge;\nimport org.shredzone.acme4j.challenge.Http01Challenge;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.util.KeyPairUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * A simple client test tool.\n * <p>\n * First check the configuration constants at the top of the class. Then run the class,\n * and pass in the names of the domains as parameters.\n * <p>\n * The tool won't run as-is. You MUST change the {@link #CA_URI} constant and set the\n * connection URI of your target CA there.\n * <p>\n * If your CA requires External Account Binding (EAB), you MUST also fill the\n * {@link #EAB_KID} and {@link #EAB_HMAC} constants with the values provided by your CA.\n * <p>\n * If your CA requires an email field to be set in your account, you also need to set\n * {@link #ACCOUNT_EMAIL}.\n * <p>\n * All other fields are optional and should work with the default values, unless your CA\n * has special requirements (e.g. to the key type).\n *\n * @see <a href=\"https://shredzone.org/maven/acme4j/example.html\">This example, fully\n * explained in the documentation.</a>\n */\npublic class ClientTest {\n    // Set the Connection URI of your CA here. For testing purposes, use a staging\n    // server if possible. Example: \"acme://letsencrypt.org/staging\" for the Let's\n    // Encrypt staging server.\n    private static final String CA_URI = \"acme://example.com/staging\";\n\n    // E-Mail address to be associated with the account. Optional, null if not used.\n    private static final String ACCOUNT_EMAIL = null;\n\n    // If the CA requires External Account Binding (EAB), set the provided KID and HMAC here.\n    private static final String EAB_KID = null;\n    private static final String EAB_HMAC = null;\n\n    // A supplier for a new account KeyPair. The default creates a new EC key pair.\n    private static final Supplier<KeyPair> ACCOUNT_KEY_SUPPLIER = KeyPairUtils::createKeyPair;\n\n    // A supplier for a new domain KeyPair. The default creates a RSA key pair.\n    private static final Supplier<KeyPair> DOMAIN_KEY_SUPPLIER = () -> KeyPairUtils.createKeyPair(4096);\n\n    // File name of the User Key Pair\n    private static final File USER_KEY_FILE = new File(\"user.key\");\n\n    // File name of the Domain Key Pair\n    private static final File DOMAIN_KEY_FILE = new File(\"domain.key\");\n\n    // File name of the signed certificate\n    private static final File DOMAIN_CHAIN_FILE = new File(\"domain-chain.crt\");\n\n    //Challenge type to be used\n    private static final ChallengeType CHALLENGE_TYPE = ChallengeType.HTTP;\n\n    // Maximum time to wait until VALID/INVALID is expected\n    private static final Duration TIMEOUT = Duration.ofSeconds(60L);\n\n    private static final Logger LOG = LoggerFactory.getLogger(ClientTest.class);\n\n    private enum ChallengeType {HTTP, DNS}\n\n    /**\n     * Generates a certificate for the given domains. Also takes care for the registration\n     * process.\n     *\n     * @param domains\n     *         Domains to get a common certificate for\n     */\n    public void fetchCertificate(Collection<String> domains) throws IOException, AcmeException, InterruptedException {\n        // Load the user key file. If there is no key file, create a new one.\n        KeyPair userKeyPair = loadOrCreateUserKeyPair();\n\n        // Create a session.\n        Session session = new Session(CA_URI);\n\n        // Get the Account.\n        // If there is no account yet, create a new one.\n        Account acct = findOrRegisterAccount(session, userKeyPair);\n\n        // Load or create a key pair for the domains. This should not be the userKeyPair!\n        KeyPair domainKeyPair = loadOrCreateDomainKeyPair();\n\n        // Order the certificate\n        Order order = acct.newOrder().domains(domains).create();\n\n        // Perform all required authorizations\n        for (Authorization auth : order.getAuthorizations()) {\n            authorize(auth);\n        }\n\n        // Wait for the order to become READY\n        order.waitUntilReady(TIMEOUT);\n\n        // Order the certificate\n        order.execute(domainKeyPair);\n\n        // Wait for the order to complete\n        Status status = order.waitForCompletion(TIMEOUT);\n        if (status != Status.VALID) {\n            LOG.error(\"Order has failed, reason: {}\", order.getError()\n                    .map(Problem::toString)\n                    .orElse(\"unknown\"));\n            throw new AcmeException(\"Order failed... Giving up.\");\n        }\n\n        // Get the certificate\n        Certificate certificate = order.getCertificate();\n\n        LOG.info(\"Success! The certificate for domains {} has been generated!\", domains);\n        LOG.info(\"Certificate URL: {}\", certificate.getLocation());\n\n        // Write a combined file containing the certificate and chain.\n        try (FileWriter fw = new FileWriter(DOMAIN_CHAIN_FILE)) {\n            certificate.writeCertificate(fw);\n        }\n\n        // That's all! Configure your web server to use the DOMAIN_KEY_FILE and\n        // DOMAIN_CHAIN_FILE for the requested domains.\n    }\n\n    /**\n     * Loads a user key pair from {@link #USER_KEY_FILE}. If the file does not exist, a\n     * new key pair is generated and saved.\n     * <p>\n     * Keep this key pair in a safe place! In a production environment, you will not be\n     * able to access your account again if you should lose the key pair.\n     *\n     * @return User's {@link KeyPair}.\n     */\n    private KeyPair loadOrCreateUserKeyPair() throws IOException {\n        if (USER_KEY_FILE.exists()) {\n            // If there is a key file, read it\n            try (FileReader fr = new FileReader(USER_KEY_FILE)) {\n                return KeyPairUtils.readKeyPair(fr);\n            }\n\n        } else {\n            // If there is none, create a new key pair and save it\n            KeyPair userKeyPair = ACCOUNT_KEY_SUPPLIER.get();\n            try (FileWriter fw = new FileWriter(USER_KEY_FILE)) {\n                KeyPairUtils.writeKeyPair(userKeyPair, fw);\n            }\n            return userKeyPair;\n        }\n    }\n\n    /**\n     * Loads a domain key pair from {@link #DOMAIN_KEY_FILE}. If the file does not exist,\n     * a new key pair is generated and saved.\n     *\n     * @return Domain {@link KeyPair}.\n     */\n    private KeyPair loadOrCreateDomainKeyPair() throws IOException {\n        if (DOMAIN_KEY_FILE.exists()) {\n            try (FileReader fr = new FileReader(DOMAIN_KEY_FILE)) {\n                return KeyPairUtils.readKeyPair(fr);\n            }\n        } else {\n            KeyPair domainKeyPair = DOMAIN_KEY_SUPPLIER.get();\n            try (FileWriter fw = new FileWriter(DOMAIN_KEY_FILE)) {\n                KeyPairUtils.writeKeyPair(domainKeyPair, fw);\n            }\n            return domainKeyPair;\n        }\n    }\n\n    /**\n     * Finds your {@link Account} at the ACME server. It will be found by your user's\n     * public key. If your key is not known to the server yet, a new account will be\n     * created.\n     * <p>\n     * This is a simple way of finding your {@link Account}. A better way is to get the\n     * URL of your new account with {@link Account#getLocation()} and store it somewhere.\n     * If you need to get access to your account later, reconnect to it via {@link\n     * Session#login(URL, KeyPair)} by using the stored location.\n     *\n     * @param session\n     *         {@link Session} to bind with\n     * @return {@link Account}\n     */\n    private Account findOrRegisterAccount(Session session, KeyPair accountKey) throws AcmeException {\n        // Ask the user to accept the TOS, if server provides us with a link.\n        Optional<URI> tos = session.getMetadata().getTermsOfService();\n        if (tos.isPresent()) {\n            acceptAgreement(tos.get());\n        }\n\n        AccountBuilder accountBuilder = new AccountBuilder()\n                .agreeToTermsOfService()\n                .useKeyPair(accountKey);\n\n        // Set your email (if available)\n        if (ACCOUNT_EMAIL != null) {\n            accountBuilder.addEmail(ACCOUNT_EMAIL);\n        }\n\n        // Use the KID and HMAC if the CA uses External Account Binding\n        if (EAB_KID != null && EAB_HMAC != null) {\n            accountBuilder.withKeyIdentifier(EAB_KID, EAB_HMAC);\n        }\n\n        Account account = accountBuilder.create(session);\n        LOG.info(\"Registered a new user, URL: {}\", account.getLocation());\n\n        return account;\n    }\n\n    /**\n     * Authorize a domain. It will be associated with your account, so you will be able to\n     * retrieve a signed certificate for the domain later.\n     *\n     * @param auth\n     *         {@link Authorization} to perform\n     */\n    private void authorize(Authorization auth) throws AcmeException, InterruptedException {\n        LOG.info(\"Authorization for domain {}\", auth.getIdentifier().getDomain());\n\n        // The authorization is already valid. No need to process a challenge.\n        if (auth.getStatus() == Status.VALID) {\n            return;\n        }\n\n        // Find the desired challenge and prepare it.\n        Challenge challenge = switch (CHALLENGE_TYPE) {\n            case HTTP -> httpChallenge(auth);\n            case DNS -> dnsChallenge(auth);\n        };\n\n        if (challenge == null) {\n            throw new AcmeException(\"No challenge found\");\n        }\n\n        // If the challenge is already verified, there's no need to execute it again.\n        if (challenge.getStatus() == Status.VALID) {\n            return;\n        }\n\n        // Now trigger the challenge.\n        challenge.trigger();\n\n        // Poll for the challenge to complete.\n        Status status = challenge.waitForCompletion(TIMEOUT);\n        if (status != Status.VALID) {\n            LOG.error(\"Challenge has failed, reason: {}\", challenge.getError()\n                    .map(Problem::toString)\n                    .orElse(\"unknown\"));\n            throw new AcmeException(\"Challenge failed... Giving up.\");\n        }\n\n        LOG.info(\"Challenge has been completed. Remember to remove the validation resource.\");\n        completeChallenge(\"Challenge has been completed.\\nYou can remove the resource again now.\");\n    }\n\n    /**\n     * Prepares a HTTP challenge.\n     * <p>\n     * The verification of this challenge expects a file with a certain content to be\n     * reachable at a given path under the domain to be tested.\n     * <p>\n     * This example outputs instructions that need to be executed manually. In a\n     * production environment, you would rather generate this file automatically, or maybe\n     * use a servlet that returns {@link Http01Challenge#getAuthorization()}.\n     *\n     * @param auth\n     *         {@link Authorization} to find the challenge in\n     * @return {@link Challenge} to verify\n     */\n    public Challenge httpChallenge(Authorization auth) throws AcmeException {\n        // Find a single http-01 challenge\n        Http01Challenge challenge = auth.findChallenge(Http01Challenge.class)\n                .orElseThrow(() -> new AcmeException(\"Found no \" + Http01Challenge.TYPE\n                        + \" challenge, don't know what to do...\"));\n\n        // Output the challenge, wait for acknowledge...\n        LOG.info(\"Please create a file in your web server's base directory.\");\n        LOG.info(\"It must be reachable at: http://{}/.well-known/acme-challenge/{}\",\n                auth.getIdentifier().getDomain(), challenge.getToken());\n        LOG.info(\"File name: {}\", challenge.getToken());\n        LOG.info(\"Content: {}\", challenge.getAuthorization());\n        LOG.info(\"The file must not contain any leading or trailing whitespaces or line breaks!\");\n        LOG.info(\"If you're ready, dismiss the dialog...\");\n\n        StringBuilder message = new StringBuilder();\n        message.append(\"Please create a file in your web server's base directory.\\n\\n\");\n        message.append(\"http://\")\n                .append(auth.getIdentifier().getDomain())\n                .append(\"/.well-known/acme-challenge/\")\n                .append(challenge.getToken())\n                .append(\"\\n\\n\");\n        message.append(\"Content:\\n\\n\");\n        message.append(challenge.getAuthorization());\n        acceptChallenge(message.toString());\n\n        return challenge;\n    }\n\n    /**\n     * Prepares a DNS challenge.\n     * <p>\n     * The verification of this challenge expects a TXT record with a certain content.\n     * <p>\n     * This example outputs instructions that need to be executed manually. In a\n     * production environment, you would rather configure your DNS automatically.\n     *\n     * @param auth\n     *         {@link Authorization} to find the challenge in\n     * @return {@link Challenge} to verify\n     */\n    public Challenge dnsChallenge(Authorization auth) throws AcmeException {\n        // Find a single dns-01 challenge\n        Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE)\n                .map(Dns01Challenge.class::cast)\n                .orElseThrow(() -> new AcmeException(\"Found no \" + Dns01Challenge.TYPE\n                        + \" challenge, don't know what to do...\"));\n\n        // Output the challenge, wait for acknowledge...\n        LOG.info(\"Please create a TXT record:\");\n        LOG.info(\"{} IN TXT {}\",\n                challenge.getRRName(auth.getIdentifier()), challenge.getDigest());\n        LOG.info(\"If you're ready, dismiss the dialog...\");\n\n        StringBuilder message = new StringBuilder();\n        message.append(\"Please create a TXT record:\\n\\n\");\n        message.append(challenge.getRRName(auth.getIdentifier()))\n                .append(\" IN TXT \")\n                .append(challenge.getDigest());\n        acceptChallenge(message.toString());\n\n        return challenge;\n    }\n\n    /**\n     * Presents the instructions for preparing the challenge validation, and waits for\n     * dismissal. If the user cancelled the dialog, an exception is thrown.\n     *\n     * @param message\n     *         Instructions to be shown in the dialog\n     */\n    public void acceptChallenge(String message) throws AcmeException {\n        int option = JOptionPane.showConfirmDialog(null,\n                message,\n                \"Prepare Challenge\",\n                JOptionPane.OK_CANCEL_OPTION);\n        if (option == JOptionPane.CANCEL_OPTION) {\n            throw new AcmeException(\"User cancelled the challenge\");\n        }\n    }\n\n    /**\n     * Presents the instructions for removing the challenge validation, and waits for\n     * dismissal.\n     *\n     * @param message\n     *         Instructions to be shown in the dialog\n     */\n    public void completeChallenge(String message) {\n        JOptionPane.showMessageDialog(null,\n                message,\n                \"Complete Challenge\",\n                JOptionPane.INFORMATION_MESSAGE);\n    }\n\n    /**\n     * Presents the user a link to the Terms of Service, and asks for confirmation. If the\n     * user denies confirmation, an exception is thrown.\n     *\n     * @param agreement\n     *         {@link URI} of the Terms of Service\n     */\n    public void acceptAgreement(URI agreement) throws AcmeException {\n        int option = JOptionPane.showConfirmDialog(null,\n                \"Do you accept the Terms of Service?\\n\\n\" + agreement,\n                \"Accept ToS\",\n                JOptionPane.YES_NO_OPTION);\n        if (option == JOptionPane.NO_OPTION) {\n            throw new AcmeException(\"User did not accept Terms of Service\");\n        }\n    }\n\n    /**\n     * Invokes this example.\n     *\n     * @param args\n     *         Domains to get a certificate for\n     */\n    public static void main(String... args) {\n        if (args.length == 0) {\n            System.err.println(\"Usage: ClientTest <domain>...\");\n            System.exit(1);\n        }\n\n        LOG.info(\"Starting up...\");\n\n        Security.addProvider(new BouncyCastleProvider());\n\n        Collection<String> domains = Arrays.asList(args);\n        try {\n            ClientTest ct = new ClientTest();\n            ct.fetchCertificate(domains);\n        } catch (Exception ex) {\n            LOG.error(\"Failed to get a certificate for domains \" + domains, ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-example/src/main/resources/.gitignore",
    "content": ""
  },
  {
    "path": "acme4j-example/src/main/resources/simplelogger.properties",
    "content": "\norg.slf4j.simpleLogger.log.org.shredzone.acme4j = debug\n"
  },
  {
    "path": "acme4j-example/src/test/java/.gitignore",
    "content": ""
  },
  {
    "path": "acme4j-example/src/test/resources/.gitignore",
    "content": ""
  },
  {
    "path": "acme4j-it/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n *\n * acme4j - ACME Java client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n *\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.shredzone.acme4j</groupId>\n        <artifactId>acme4j</artifactId>\n        <version>5.1.1-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>acme4j-it</artifactId>\n\n    <name>acme4j IT</name>\n    <description>acme4j Integration Tests</description>\n\n    <properties>\n        <pebble.version>latest</pebble.version>\n\n        <skipITs>true</skipITs>\n    </properties>\n\n    <profiles>\n        <profile>\n            <!-- Profile with integration tests. Requires docker! -->\n            <!-- mvn -P ci verify -->\n            <id>ci</id>\n            <properties>\n                <skipITs>false</skipITs>\n            </properties>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-failsafe-plugin</artifactId>\n                        <configuration>\n                            <excludes>\n                                <exclude>org/shredzone/acme4j/it/boulder/**</exclude>\n                            </excludes>\n                        </configuration>\n                    </plugin>\n                    <plugin>\n                        <groupId>io.fabric8</groupId>\n                        <artifactId>docker-maven-plugin</artifactId>\n                        <executions>\n                            <execution>\n                                <id>start</id>\n                                <phase>pre-integration-test</phase>\n                                <goals>\n                                    <goal>build</goal>\n                                    <goal>start</goal>\n                                </goals>\n                            </execution>\n                            <execution>\n                                <id>stop</id>\n                                <phase>post-integration-test</phase>\n                                <goals>\n                                    <goal>stop</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n        <profile>\n            <!-- Profile for testing against a local Boulder server. -->\n            <!-- mvn -P boulder verify -->\n            <id>boulder</id>\n            <properties>\n                <skipITs>false</skipITs>\n            </properties>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-failsafe-plugin</artifactId>\n                        <configuration>\n                            <excludes>\n                                <exclude>org/shredzone/acme4j/it/pebble/**</exclude>\n                            </excludes>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>io.fabric8</groupId>\n                <artifactId>docker-maven-plugin</artifactId>\n                <version>0.48.0</version>\n\n                <configuration>\n                    <logStdout>true</logStdout>\n                    <verbose>true</verbose>\n                    <removeAll>true</removeAll>\n                    <containerNamePattern>%a</containerNamePattern>\n                    <images>\n                        <image>\n                            <alias>pebble</alias>\n                            <name>ghcr.io/letsencrypt/pebble:latest</name>\n                            <run>\n                                <ports>\n                                    <port>14000:14000</port><!-- ACME API -->\n                                    <port>15000:15000</port><!-- Management API -->\n                                </ports>\n                                <links>\n                                    <link>bammbamm</link>\n                                </links>\n                                <cmd>\n                                    <exec>\n                                        <arg>-strict</arg>\n                                        <arg>-dnsserver</arg>\n                                        <arg>bammbamm:8053</arg>\n                                        <arg>-config</arg>\n                                        <arg>/test/config/pebble-config.json</arg>\n                                    </exec>\n                                </cmd>\n                                <wait>\n                                    <log>Listening</log>\n                                </wait>\n                                <!-- Comment out to perform tests with validation delays. -->\n                                <env>\n                                    <PEBBLE_VA_NOSLEEP>1</PEBBLE_VA_NOSLEEP>\n                                </env>\n                            </run>\n                        </image>\n                        <image>\n                            <alias>bammbamm</alias>\n                            <name>acme4j/challtestsrv:${project.version}</name>\n                            <build>\n                                <!-- Workaround for https://github.com/letsencrypt/pebble/issues/418 -->\n                                <dockerFile>challtestsrv.dockerfile</dockerFile>\n                            </build>\n                            <run>\n                                <hostname>bammbamm</hostname>\n                                <ports>\n                                    <port>8055:8055</port>\n                                </ports>\n                                <wait>\n                                    <log>Starting management server</log>\n                                </wait>\n                            </run>\n                        </image>\n                    </images>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.shredzone.acme4j</groupId>\n            <artifactId>acme4j-client</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpclient</artifactId>\n            <version>${httpclient.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <version>${slf4j.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-simple</artifactId>\n            <version>${slf4j.version}</version>\n        </dependency>\n    </dependencies>\n</project>\n"
  },
  {
    "path": "acme4j-it/src/main/docker/challtestsrv.dockerfile",
    "content": "FROM ghcr.io/letsencrypt/pebble-challtestsrv:latest\n\nFROM alpine\nCOPY --from=0 /app /app\nCOPY challtestsrv.sh /\nENTRYPOINT [ \"/challtestsrv.sh\" ]"
  },
  {
    "path": "acme4j-it/src/main/docker/challtestsrv.sh",
    "content": "#!/bin/sh\n\nBAMMBAMM_IP=$(hostname -i)\necho \"My IP is: $BAMMBAMM_IP\"\n\n/app -defaultIPv6 \"\" -defaultIPv4 \"$BAMMBAMM_IP\"\n"
  },
  {
    "path": "acme4j-it/src/main/java/module-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\nmodule org.shredzone.acme4j.it {\n    requires org.shredzone.acme4j;\n\n    requires com.github.spotbugs.annotations;\n    requires org.apache.httpcomponents.httpclient;\n    requires org.apache.httpcomponents.httpcore;\n\n    exports org.shredzone.acme4j.it;\n}\n"
  },
  {
    "path": "acme4j-it/src/main/java/org/shredzone/acme4j/it/BammBammClient.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.it;\n\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.Objects;\n\nimport org.apache.http.HttpStatus;\nimport org.apache.http.client.ClientProtocolException;\nimport org.apache.http.client.HttpClient;\nimport org.apache.http.client.methods.HttpPost;\nimport org.apache.http.entity.ContentType;\nimport org.apache.http.entity.StringEntity;\nimport org.apache.http.impl.client.HttpClients;\nimport org.apache.http.util.EntityUtils;\nimport org.shredzone.acme4j.toolbox.JSONBuilder;\n\n/**\n * The BammBamm client connects to the pebble-challtestsrv.\n */\npublic class BammBammClient {\n    private static final HttpClient CLIENT = HttpClients.createDefault();\n\n    private final String baseUrl;\n\n    /**\n     * Creates a new BammBamm client.\n     *\n     * @param baseUrl\n     *            Base URL of the pebble-challtestsrv server to connect to.\n     */\n    public BammBammClient(String baseUrl) {\n        this.baseUrl = Objects.requireNonNull(baseUrl) + '/';\n    }\n\n    /**\n     * Adds a HTTP token.\n     *\n     * @param token\n     *            Token to add\n     * @param challenge\n     *            Challenge to respond with\n     */\n    public void httpAddToken(String token, String challenge) throws IOException {\n        var jb = new JSONBuilder();\n        jb.put(\"token\", token);\n        jb.put(\"content\", challenge);\n        sendRequest(\"add-http01\", jb.toString());\n    }\n\n    /**\n     * Removes a HTTP token.\n     *\n     * @param token\n     *            Token to remove\n     */\n    public void httpRemoveToken(String token) throws IOException {\n        var jb = new JSONBuilder();\n        jb.put(\"token\", token);\n        sendRequest(\"del-http01\", jb.toString());\n    }\n\n    /**\n     * Adds an A Record to the DNS. Only one A Record is supported per domain. If another\n     * A Record is set, it will replace the existing one.\n     *\n     * @param domain\n     *            Domain of the A Record\n     * @param ip\n     *            IP address or domain name. If a domain name is used, it will be resolved\n     *            and the IP will be used.\n     */\n    public void dnsAddARecord(String domain, String ip) throws IOException {\n        var jb = new JSONBuilder();\n        jb.put(\"host\", domain);\n        jb.array(\"addresses\", Collections.singletonList(ip));\n        sendRequest(\"add-a\", jb.toString());\n    }\n\n    /**\n     * Removes an A Record from the DNS.\n     *\n     * @param domain\n     *            Domain to remove the A Record from\n     */\n    public void dnsRemoveARecord(String domain) throws IOException {\n        var jb = new JSONBuilder();\n        jb.put(\"host\", domain);\n        sendRequest(\"clear-a\", jb.toString());\n    }\n\n    /**\n     * Adds a TXT Record to the DNS. Only one TXT Record is supported per domain. If\n     * another TXT Record is set, it will replace the existing one.\n     *\n     * @param domain\n     *            Domain name to add the TXT Record to\n     * @param txt\n     *            TXT record to add\n     */\n    public void dnsAddTxtRecord(String domain, String txt) throws IOException {\n        var jb = new JSONBuilder();\n        jb.put(\"host\", domain);\n        jb.put(\"value\", txt);\n        sendRequest(\"set-txt\", jb.toString());\n    }\n\n    /**\n     * Removes a TXT Record from the DNS.\n     *\n     * @param domain\n     *            Domain to remove the TXT Record from\n     */\n    public void dnsRemoveTxtRecord(String domain) throws IOException {\n        var jb = new JSONBuilder();\n        jb.put(\"host\", domain);\n        sendRequest(\"clear-txt\", jb.toString());\n    }\n\n    /**\n     * Adds a CNAME Record to the DNS. Only one CNAME Record is supported per domain. If\n     * another CNAME Record is set, it will replace the existing one.\n     *\n     * @param domain\n     *         Domain to add the CNAME Record to\n     * @param cname\n     *         CNAME Record to add\n     * @since 2.9\n     */\n    public void dnsAddCnameRecord(String domain, String cname) throws IOException {\n        var jb = new JSONBuilder();\n        jb.put(\"host\", domain);\n        jb.put(\"target\", cname);\n        sendRequest(\"set-cname\", jb.toString());\n    }\n\n    /**\n     * Removes a CNAME Record from the DNS.\n     *\n     * @param domain\n     *         Domain to remove the CNAME Record from\n     * @since 2.9\n     */\n    public void dnsRemoveCnameRecord(String domain) throws IOException {\n        var jb = new JSONBuilder();\n        jb.put(\"host\", domain);\n        sendRequest(\"clear-cname\", jb.toString());\n    }\n\n    /**\n     * Simulates a SERVFAIL for the given domain.\n     *\n     * @param domain\n     *         Domain that will give a SERVFAIL response\n     * @since 2.9\n     */\n    public void dnsAddServFailRecord(String domain) throws IOException {\n        var jb = new JSONBuilder();\n        jb.put(\"host\", domain);\n        sendRequest(\"set-servfail\", jb.toString());\n    }\n\n    /**\n     * Removes a SERVFAIL Record from the DNS.\n     *\n     * @param domain\n     *         Domain to remove the SEVFAIL Record from\n     * @since 2.9\n     */\n    public void dnsRemoveServFailRecord(String domain) throws IOException {\n        var jb = new JSONBuilder();\n        jb.put(\"host\", domain);\n        sendRequest(\"clear-servfail\", jb.toString());\n    }\n\n    /**\n     * Adds a certificate for TLS-ALPN tests.\n     *\n     * @param domain\n     *            Certificate domain to be added\n     * @param keyauth\n     *            Key authorization to be used for validation\n     */\n    public void tlsAlpnAddCertificate(String domain, String keyauth) throws IOException {\n        var jb = new JSONBuilder();\n        jb.put(\"host\", domain);\n        jb.put(\"content\", keyauth);\n        sendRequest(\"add-tlsalpn01\", jb.toString());\n    }\n\n    /**\n     * Removes a certificate.\n     *\n     * @param domain\n     *            Certificate domain to be removed\n     */\n    public void tlsAlpnRemoveCertificate(String domain) throws IOException {\n        var jb = new JSONBuilder();\n        jb.put(\"host\", domain);\n        sendRequest(\"del-tlsalpn01\", jb.toString());\n    }\n\n    /**\n     * Sends a request to the pebble-challtestsrv.\n     *\n     * @param call\n     *            Endpoint to be called\n     * @param body\n     *            JSON body\n     */\n    private void sendRequest(String call, String body) throws IOException {\n        try {\n            var httppost = new HttpPost(baseUrl + call);\n            httppost.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));\n\n            var response = CLIENT.execute(httppost);\n\n            EntityUtils.consume(response.getEntity());\n\n            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {\n                throw new IOException(response.getStatusLine().getReasonPhrase());\n            }\n        } catch (ClientProtocolException ex) {\n            throw new IOException(ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-it/src/main/java/org/shredzone/acme4j/it/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.it;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-it/src/main/resources/.gitignore",
    "content": ""
  },
  {
    "path": "acme4j-it/src/test/java/org/shredzone/acme4j/it/ProviderIT.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2024 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.it;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatNoException;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.time.Duration;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.exception.AcmeException;\n\n/**\n * A very simple test to check if all provider URIs are still pointing to a directory\n * resource.\n * <p>\n * If one of these tests fails, it could be an indicator that the corresponding directory\n * URL has been changed on CA side, or that EAR or auto-renewal features have been\n * changed.\n * <p>\n * These integration tests require a network connection.\n */\npublic class ProviderIT {\n\n    /**\n     * Test Actalis\n     */\n    @Test\n    public void testActalis() throws AcmeException, MalformedURLException {\n        var session = new Session(\"acme://actalis.com\");\n        assertThat(session.getMetadata().getWebsite()).hasValue(URI.create(\"https://www.actalis.com\").toURL());\n        assertThatNoException().isThrownBy(() -> session.resourceUrl(Resource.NEW_ACCOUNT));\n        assertThat(session.getMetadata().isExternalAccountRequired()).isTrue();\n        assertThat(session.getMetadata().isAutoRenewalEnabled()).isFalse();\n        assertThat(session.resourceUrlOptional(Resource.RENEWAL_INFO)).isNotEmpty();\n    }\n\n    /**\n     * Test Google CA\n     */\n    @Test\n    public void testGoogle() throws AcmeException, MalformedURLException {\n        var session = new Session(\"acme://pki.goog\");\n        assertThat(session.getMetadata().getWebsite()).hasValue(URI.create(\"https://pki.goog\").toURL());\n        assertThatNoException().isThrownBy(() -> session.resourceUrl(Resource.NEW_ACCOUNT));\n        assertThat(session.getMetadata().isExternalAccountRequired()).isTrue();\n        assertThat(session.getMetadata().isAutoRenewalEnabled()).isFalse();\n        assertThat(session.resourceUrlOptional(Resource.RENEWAL_INFO)).isNotEmpty();\n\n        var sessionStage = new Session(\"acme://pki.goog/staging\");\n        assertThat(sessionStage.getMetadata().getWebsite()).hasValue(URI.create(\"https://pki.goog\").toURL());\n        assertThatNoException().isThrownBy(() -> sessionStage.resourceUrl(Resource.NEW_ACCOUNT));\n        assertThat(sessionStage.getMetadata().isExternalAccountRequired()).isTrue();\n        assertThat(sessionStage.getMetadata().isAutoRenewalEnabled()).isFalse();\n        assertThat(sessionStage.resourceUrlOptional(Resource.RENEWAL_INFO)).isNotEmpty();\n    }\n\n    /**\n     * Test Let's Encrypt\n     */\n    @Test\n    public void testLetsEncrypt() throws AcmeException, MalformedURLException {\n        var session = new Session(\"acme://letsencrypt.org\");\n        assertThat(session.getMetadata().getWebsite()).hasValue(URI.create(\"https://letsencrypt.org\").toURL());\n        assertThatNoException().isThrownBy(() -> session.resourceUrl(Resource.NEW_ACCOUNT));\n        assertThat(session.getMetadata().isExternalAccountRequired()).isFalse();\n        assertThat(session.getMetadata().isAutoRenewalEnabled()).isFalse();\n        assertThat(session.resourceUrlOptional(Resource.RENEWAL_INFO)).isNotEmpty();\n\n        var sessionStage = new Session(\"acme://letsencrypt.org/staging\");\n        assertThat(sessionStage.getMetadata().getWebsite()).hasValue(URI.create(\"https://letsencrypt.org/docs/staging-environment/\").toURL());\n        assertThatNoException().isThrownBy(() -> sessionStage.resourceUrl(Resource.NEW_ACCOUNT));\n        assertThat(sessionStage.getMetadata().isExternalAccountRequired()).isFalse();\n        assertThat(sessionStage.getMetadata().isAutoRenewalEnabled()).isFalse();\n        assertThat(sessionStage.resourceUrlOptional(Resource.RENEWAL_INFO)).isNotEmpty();\n    }\n\n    /**\n     * Test Pebble\n     */\n    @Test\n    public void testPebble() throws AcmeException, MalformedURLException {\n        var pebbleHost = System.getProperty(\"pebbleHost\", \"localhost\");\n        var pebblePort = System.getProperty(\"pebblePort\", \"14000\");\n\n        var session = new Session(\"acme://pebble/\" + pebbleHost + \":\" + pebblePort);\n        assertThat(session.getMetadata().getWebsite()).isEmpty();\n        assertThatNoException().isThrownBy(() -> session.resourceUrl(Resource.NEW_ACCOUNT));\n        assertThat(session.getMetadata().isExternalAccountRequired()).isFalse();\n        assertThat(session.getMetadata().isAutoRenewalEnabled()).isFalse();\n        assertThat(session.resourceUrlOptional(Resource.RENEWAL_INFO)).isNotEmpty();\n    }\n\n    /**\n     * Test ssl.com, production\n     */\n    @Test\n    public void testSslCom() throws AcmeException, MalformedURLException {\n        var sessionEcc = new Session(\"acme://ssl.com/ecc\");\n        assertThat(sessionEcc.getMetadata().getWebsite()).hasValue(URI.create(\"https://www.ssl.com\").toURL());\n        assertThatNoException().isThrownBy(() -> sessionEcc.resourceUrl(Resource.NEW_ACCOUNT));\n        assertThat(sessionEcc.getMetadata().isExternalAccountRequired()).isTrue();\n        assertThat(sessionEcc.getMetadata().isAutoRenewalEnabled()).isFalse();\n        assertThat(sessionEcc.resourceUrlOptional(Resource.RENEWAL_INFO)).isNotEmpty();\n\n        var sessionRsa = new Session(\"acme://ssl.com/rsa\");\n        assertThat(sessionRsa.getMetadata().getWebsite()).hasValue(URI.create(\"https://www.ssl.com\").toURL());\n        assertThatNoException().isThrownBy(() -> sessionRsa.resourceUrl(Resource.NEW_ACCOUNT));\n        assertThat(sessionRsa.getMetadata().isExternalAccountRequired()).isTrue();\n        assertThat(sessionRsa.getMetadata().isAutoRenewalEnabled()).isFalse();\n        assertThat(sessionRsa.resourceUrlOptional(Resource.RENEWAL_INFO)).isNotEmpty();\n\n        // If this test fails, the metadata has been fixed on server side. Then remove\n        // the patch at ZeroSSLAcmeProvider, and update the documentation.\n        var sessionEABCheck = new Session(\"https://acme.ssl.com/sslcom-dv-ecc\");\n        assertThat(sessionEABCheck.getMetadata().isExternalAccountRequired()).isFalse();\n    }\n\n    /**\n     * Test ssl.com, staging server\n     */\n    @Test\n    @SoftFail(\"Frequent certificate expiration of acme-try.ssl.com\")\n    public void testSslComStaging() throws AcmeException, MalformedURLException {\n        var sessionEccStage = new Session(\"acme://ssl.com/staging/ecc\");\n        assertThat(sessionEccStage.getMetadata().getWebsite()).hasValue(URI.create(\"https://www.ssl.com\").toURL());\n        assertThatNoException().isThrownBy(() -> sessionEccStage.resourceUrl(Resource.NEW_ACCOUNT));\n        assertThat(sessionEccStage.getMetadata().isExternalAccountRequired()).isTrue();\n        assertThat(sessionEccStage.getMetadata().isAutoRenewalEnabled()).isFalse();\n        assertThat(sessionEccStage.resourceUrlOptional(Resource.RENEWAL_INFO)).isNotEmpty();\n\n        var sessionRsaStage = new Session(\"acme://ssl.com/staging/rsa\");\n        assertThat(sessionRsaStage.getMetadata().getWebsite()).hasValue(URI.create(\"https://www.ssl.com\").toURL());\n        assertThatNoException().isThrownBy(() -> sessionRsaStage.resourceUrl(Resource.NEW_ACCOUNT));\n        assertThat(sessionRsaStage.getMetadata().isExternalAccountRequired()).isTrue();\n        assertThat(sessionRsaStage.getMetadata().isAutoRenewalEnabled()).isFalse();\n        assertThat(sessionRsaStage.resourceUrlOptional(Resource.RENEWAL_INFO)).isNotEmpty();\n\n        // If this test fails, the metadata has been fixed on server side. Then remove\n        // the patch at ZeroSSLAcmeProvider, and update the documentation.\n        var sessionEABCheckStage = new Session(\"https://acme-try.ssl.com/sslcom-dv-ecc\");\n        assertThat(sessionEABCheckStage.getMetadata().isExternalAccountRequired()).isFalse();\n    }\n\n    /**\n     * Test ZeroSSL\n     */\n    @Test\n    @SoftFail(\"Frequent network timeouts or HTTP errors\")\n    public void testZeroSsl() throws AcmeException, MalformedURLException {\n        var session = new Session(\"acme://zerossl.com\");\n        session.networkSettings().setTimeout(Duration.ofSeconds(120L));\n        assertThat(session.getMetadata().getWebsite()).hasValue(URI.create(\"https://zerossl.com\").toURL());\n        assertThatNoException().isThrownBy(() -> session.resourceUrl(Resource.NEW_ACCOUNT));\n        assertThat(session.getMetadata().isExternalAccountRequired()).isTrue();\n        assertThat(session.getMetadata().isAutoRenewalEnabled()).isFalse();\n        assertThat(session.resourceUrlOptional(Resource.RENEWAL_INFO)).isNotEmpty();\n\n        // ZeroSSL has no documented staging server (as of February 2024)\n    }\n\n}\n"
  },
  {
    "path": "acme4j-it/src/test/java/org/shredzone/acme4j/it/SoftFail.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2025 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.it;\n\nimport static java.lang.annotation.ElementType.METHOD;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.shredzone.acme4j.exception.AcmeException;\n\n/**\n * Marks a test to fail softly if an {@link AcmeException} is thrown. These are usually\n * integration tests that fail frequently because the external server has stability\n * issues.\n */\n@Retention(RUNTIME)\n@Target(METHOD)\n@ExtendWith(SoftFailExtension.class)\npublic @interface SoftFail {\n    /**\n     * A human-readable reason why this test is marked as soft fail.\n     */\n    String value();\n}\n"
  },
  {
    "path": "acme4j-it/src/test/java/org/shredzone/acme4j/it/SoftFailExtension.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2025 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.it;\n\nimport org.junit.jupiter.api.extension.ExtensionContext;\nimport org.junit.jupiter.api.extension.TestExecutionExceptionHandler;\nimport org.opentest4j.TestAbortedException;\nimport org.shredzone.acme4j.exception.AcmeException;\n\n/**\n * Aborts a @{@link SoftFail} annotated test when an {@link AcmeException} is thrown.\n */\npublic class SoftFailExtension implements TestExecutionExceptionHandler {\n    @Override\n    public void handleTestExecutionException(ExtensionContext ctx, Throwable ex)\n            throws Throwable {\n        if (ex instanceof AcmeException) {\n            throw new TestAbortedException(\"SOFT FAIL: \" + ctx.getDisplayName()\n                    + \" - \" + ex.getMessage(), ex);\n        }\n        throw ex;\n    }\n}\n"
  },
  {
    "path": "acme4j-it/src/test/java/org/shredzone/acme4j/it/boulder/OrderHttpIT.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.it.boulder;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.net.URI;\nimport java.security.KeyPair;\nimport java.time.Duration;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.AccountBuilder;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.challenge.Http01Challenge;\nimport org.shredzone.acme4j.it.BammBammClient;\nimport org.shredzone.acme4j.util.KeyPairUtils;\n\n/**\n * Tests a complete certificate order with different challenges.\n */\npublic class OrderHttpIT {\n\n    private static final String TEST_DOMAIN = \"example.com\";\n    private static final Duration TIMEOUT = Duration.ofSeconds(30L);\n\n    private final String bammbammUrl = System.getProperty(\"bammbammUrl\", \"http://localhost:14001\");\n\n    private final BammBammClient client = new BammBammClient(bammbammUrl);\n\n    /**\n     * Test if a certificate can be ordered via http-01 challenge.\n     */\n    @Test\n    public void testHttpValidation() throws Exception {\n        var session = new Session(boulderURI());\n        var keyPair = createKeyPair();\n\n        var account = new AccountBuilder()\n                    .agreeToTermsOfService()\n                    .useKeyPair(keyPair)\n                    .create(session);\n\n        var domainKeyPair = createKeyPair();\n\n        var order = account.newOrder().domain(TEST_DOMAIN).create();\n\n        for (var auth : order.getAuthorizations()) {\n            var challenge = auth.findChallenge(Http01Challenge.class).orElseThrow();\n\n            client.httpAddToken(challenge.getToken(), challenge.getAuthorization());\n\n            challenge.trigger();\n            challenge.waitForCompletion(TIMEOUT);\n\n            assertThat(challenge.getStatus()).isEqualTo(Status.VALID);\n            assertThat(auth.getStatus()).isEqualTo(Status.VALID);\n\n            client.httpRemoveToken(challenge.getToken());\n        }\n\n        order.waitUntilReady(TIMEOUT);\n        assertThat(order.getStatus()).isEqualTo(Status.READY);\n\n        order.execute(domainKeyPair);\n        order.waitForCompletion(TIMEOUT);\n        assertThat(order.getStatus()).isEqualTo(Status.VALID);\n\n        var cert = order.getCertificate().getCertificate();\n        assertThat(cert.getNotAfter()).isNotNull();\n        assertThat(cert.getNotBefore()).isNotNull();\n        assertThat(cert.getSubjectX500Principal().getName()).contains(\"CN=\" + TEST_DOMAIN);\n    }\n\n    /**\n     * @return The {@link URI} of the Boulder server to test against.\n     */\n    protected URI boulderURI() {\n        return URI.create(\"http://localhost:4001/directory\");\n    }\n\n    /**\n     * Creates a fresh key pair.\n     *\n     * @return Created new {@link KeyPair}\n     */\n    protected KeyPair createKeyPair() {\n        return KeyPairUtils.createKeyPair(2048);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-it/src/test/java/org/shredzone/acme4j/it/pebble/AccountIT.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.it.pebble;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.net.URI;\nimport java.security.KeyPair;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Account;\nimport org.shredzone.acme4j.AccountBuilder;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeServerException;\nimport org.shredzone.acme4j.exception.AcmeUnauthorizedException;\n\n/**\n * Account related integration tests.\n */\npublic class AccountIT extends PebbleITBase {\n\n    /**\n     * Create a new account, then bind it to a second session.\n     */\n    @Test\n    public void testCreate() throws AcmeException {\n        var keyPair = createKeyPair();\n        var session = new Session(pebbleURI());\n\n        // Register a new user\n        var login = new AccountBuilder()\n                        .addContact(\"mailto:acme@example.com\")\n                        .agreeToTermsOfService()\n                        .useKeyPair(keyPair)\n                        .createLogin(session);\n\n        var location = login.getAccount().getLocation();\n        assertIsPebbleUrl(location);\n\n        // Check registered data\n        var acct = login.getAccount();\n        assertThat(acct.getLocation()).isEqualTo(location);\n        assertThat(acct.getContacts()).contains(URI.create(\"mailto:acme@example.com\"));\n        assertThat(acct.getStatus()).isEqualTo(Status.VALID);\n\n        // Bind another Account object\n        var session2 = new Session(pebbleURI());\n        var login2 = new Login(location, keyPair, session2);\n        var acct2 = login2.getAccount();\n        assertThat(acct2.getLocation()).isEqualTo(location);\n        assertThat(acct2.getContacts()).contains(URI.create(\"mailto:acme@example.com\"));\n        assertThat(acct2.getStatus()).isEqualTo(Status.VALID);\n    }\n\n    /**\n     * Register the same key pair twice.\n     */\n    @Test\n    public void testReCreate() throws AcmeException {\n        var keyPair = createKeyPair();\n\n        // Register a new user\n        var session1 = new Session(pebbleURI());\n        var login1 = new AccountBuilder()\n                        .addContact(\"mailto:acme@example.com\")\n                        .agreeToTermsOfService()\n                        .useKeyPair(keyPair)\n                        .createLogin(session1);\n\n        var location1 = login1.getAccount().getLocation();\n        assertIsPebbleUrl(location1);\n\n        // Try to register the same account again\n        var session2 = new Session(pebbleURI());\n        var login2 = new AccountBuilder()\n                        .addContact(\"mailto:acme@example.com\")\n                        .agreeToTermsOfService()\n                        .useKeyPair(keyPair)\n                        .createLogin(session2);\n\n        var location2 = login2.getAccount().getLocation();\n        assertIsPebbleUrl(location2);\n\n        assertThat(location1).isEqualTo(location2);\n    }\n\n    /**\n     * Create a new account. Locate it via onlyExisting.\n     */\n    @Test\n    public void testCreateOnlyExisting() throws AcmeException {\n        var keyPair = createKeyPair();\n\n        var session1 = new Session(pebbleURI());\n        var login1 = new AccountBuilder()\n                        .agreeToTermsOfService()\n                        .useKeyPair(keyPair)\n                        .createLogin(session1);\n\n        var location1 = login1.getAccount().getLocation();\n        assertIsPebbleUrl(location1);\n\n        var session2 = new Session(pebbleURI());\n        var login2 = new AccountBuilder()\n                        .onlyExisting()\n                        .useKeyPair(keyPair)\n                        .createLogin(session2);\n\n        var location2 = login2.getAccount().getLocation();\n        assertIsPebbleUrl(location2);\n\n        assertThat(location1).isEqualTo(location2);\n    }\n\n    /**\n     * Locate a non-existing account via onlyExisting. Make sure an accountDoesNotExist\n     * error is returned.\n     */\n    @Test\n    public void testNotExisting() {\n        var ex = assertThrows(AcmeServerException.class, () -> {\n            KeyPair keyPair = createKeyPair();\n            Session session = new Session(pebbleURI());\n            new AccountBuilder().onlyExisting().useKeyPair(keyPair).create(session);\n        });\n        assertThat(ex.getType()).isEqualTo(URI.create(\"urn:ietf:params:acme:error:accountDoesNotExist\"));\n    }\n\n    /**\n     * Modify the contacts of an account.\n     */\n    @Test\n    public void testModify() throws AcmeException {\n        var keyPair = createKeyPair();\n        var session = new Session(pebbleURI());\n\n        var acct = new AccountBuilder()\n                        .addContact(\"mailto:acme@example.com\")\n                        .agreeToTermsOfService()\n                        .useKeyPair(keyPair)\n                        .create(session);\n        var location = acct.getLocation();\n        assertIsPebbleUrl(location);\n\n        acct.modify().addContact(\"mailto:acme2@example.com\").commit();\n\n        assertThat(acct.getContacts()).contains(\n                        URI.create(\"mailto:acme@example.com\"),\n                        URI.create(\"mailto:acme2@example.com\"));\n\n        // Still the same after updating\n        acct.fetch();\n        assertThat(acct.getContacts()).contains(\n                        URI.create(\"mailto:acme@example.com\"),\n                        URI.create(\"mailto:acme2@example.com\"));\n    }\n\n    /**\n     * Change the account key.\n     */\n    @Test\n    public void testKeyChange() throws AcmeException {\n        var keyPair = createKeyPair();\n        var session = new Session(pebbleURI());\n\n        var acct = new AccountBuilder()\n                        .agreeToTermsOfService()\n                        .useKeyPair(keyPair)\n                        .create(session);\n        var location = acct.getLocation();\n\n        var newKeyPair = createKeyPair();\n        acct.changeKey(newKeyPair);\n\n        assertThrows(AcmeServerException.class, () -> {\n            Session sessionOldKey = new Session(pebbleURI());\n            Account oldAccount = sessionOldKey.login(location, keyPair).getAccount();\n            oldAccount.fetch();\n        }, \"Old account key is still accessible\");\n\n        var sessionNewKey = new Session(pebbleURI());\n        var newAccount = sessionNewKey.login(location, newKeyPair).getAccount();\n        assertThat(newAccount.getStatus()).isEqualTo(Status.VALID);\n    }\n\n    /**\n     * Deactivate an account.\n     */\n    @Test\n    public void testDeactivate() throws AcmeException {\n        var keyPair = createKeyPair();\n        var session = new Session(pebbleURI());\n\n        var acct = new AccountBuilder()\n                        .agreeToTermsOfService()\n                        .useKeyPair(keyPair)\n                        .create(session);\n        var location = acct.getLocation();\n\n        acct.deactivate();\n\n        // Make sure it is deactivated now...\n        assertThat(acct.getStatus()).isEqualTo(Status.DEACTIVATED);\n\n        // Make sure account cannot be accessed any more...\n        var ex = assertThrows(AcmeUnauthorizedException.class,\n                () -> {\n            Session session2 = new Session(pebbleURI());\n            Account acct2 = session2.login(location, keyPair).getAccount();\n            acct2.fetch();\n        }, \"Account can still be accessed\");\n        assertThat(ex.getMessage()).isEqualTo(\"Account has been deactivated\");\n    }\n\n}\n"
  },
  {
    "path": "acme4j-it/src/test/java/org/shredzone/acme4j/it/pebble/OrderIT.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.it.pebble;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.net.URI;\nimport java.security.KeyPair;\nimport java.security.cert.X509Certificate;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.temporal.ChronoUnit;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.NullSource;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.shredzone.acme4j.AccountBuilder;\nimport org.shredzone.acme4j.Authorization;\nimport org.shredzone.acme4j.Certificate;\nimport org.shredzone.acme4j.RevocationReason;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.challenge.Challenge;\nimport org.shredzone.acme4j.challenge.Dns01Challenge;\nimport org.shredzone.acme4j.challenge.DnsAccount01Challenge;\nimport org.shredzone.acme4j.challenge.DnsPersist01Challenge;\nimport org.shredzone.acme4j.challenge.Http01Challenge;\nimport org.shredzone.acme4j.challenge.TlsAlpn01Challenge;\nimport org.shredzone.acme4j.exception.AcmeServerException;\n\n/**\n * Tests a complete certificate order with different challenges.\n */\npublic class OrderIT extends PebbleITBase {\n\n    private static final String TEST_DOMAIN = \"example.com\";\n    private static final Duration TIMEOUT = Duration.ofSeconds(30L);\n\n    /**\n     * Test if a certificate can be ordered via http-01 challenge.\n     */\n    @ParameterizedTest\n    @NullSource\n    @ValueSource(strings = {\"default\", \"shortlived\"})\n    public void testHttpValidation(String profile) throws Exception {\n        orderCertificate(TEST_DOMAIN, auth -> {\n            var client = getBammBammClient();\n\n            var challenge = auth.findChallenge(Http01Challenge.class).orElseThrow();\n\n            client.httpAddToken(challenge.getToken(), challenge.getAuthorization());\n\n            cleanup(() -> client.httpRemoveToken(challenge.getToken()));\n\n            return challenge;\n        }, OrderIT::standardRevoker, profile);\n    }\n\n    /**\n     * Test if a certificate can be ordered via dns-01 challenge.\n     */\n    @ParameterizedTest\n    @NullSource\n    @ValueSource(strings = {\"default\", \"shortlived\"})\n    public void testDnsValidation(String profile) throws Exception {\n        orderCertificate(TEST_DOMAIN, auth -> {\n            var client = getBammBammClient();\n\n            var challenge = auth.findChallenge(Dns01Challenge.class).orElseThrow();\n\n            var challengeDomainName = challenge.getRRName(auth.getIdentifier());\n\n            client.dnsAddTxtRecord(challengeDomainName, challenge.getDigest());\n\n            cleanup(() -> client.dnsRemoveTxtRecord(challengeDomainName));\n\n            return challenge;\n        }, OrderIT::standardRevoker, profile);\n    }\n\n    /**\n     * Test if a certificate can be ordered via dns-account-01 challenge.\n     */\n    @ParameterizedTest\n    @NullSource\n    @ValueSource(strings = {\"default\", \"shortlived\"})\n    public void testDnsAccountValidation(String profile) throws Exception {\n        orderCertificate(TEST_DOMAIN, auth -> {\n            var client = getBammBammClient();\n\n            var challenge = auth.findChallenge(DnsAccount01Challenge.class).orElseThrow();\n\n            var challengeDomainName = challenge.getRRName(auth.getIdentifier());\n\n            client.dnsAddTxtRecord(challengeDomainName, challenge.getDigest());\n\n            cleanup(() -> client.dnsRemoveTxtRecord(challengeDomainName));\n\n            return challenge;\n        }, OrderIT::standardRevoker, profile);\n    }\n\n    /**\n     * Test if a certificate can be ordered via dns-persist-01 challenge.\n     */\n    @ParameterizedTest\n    @NullSource\n    @ValueSource(strings = {\"default\", \"shortlived\"})\n    public void testDnsPersistValidation(String profile) throws Exception {\n        orderCertificate(TEST_DOMAIN, auth -> {\n            var client = getBammBammClient();\n\n            var challenge = auth.findChallenge(DnsPersist01Challenge.class).orElseThrow();\n\n            var challengeDomainName = challenge.getRRName(auth.getIdentifier());\n\n            // Needs to be noQuotes() because of bammbamm\n            client.dnsAddTxtRecord(challengeDomainName, challenge.buildRData().noQuotes().build());\n\n            cleanup(() -> client.dnsRemoveTxtRecord(challengeDomainName));\n\n            return challenge;\n        }, OrderIT::standardRevoker, profile);\n    }\n\n    /**\n     * Test if a certificate can be ordered via tns-alpn-01 challenge.\n     */\n    @ParameterizedTest\n    @NullSource\n    @ValueSource(strings = {\"default\", \"shortlived\"})\n    public void testTlsAlpnValidation(String profile) throws Exception {\n        orderCertificate(TEST_DOMAIN, auth -> {\n            var client = getBammBammClient();\n\n            var challenge = auth.findChallenge(TlsAlpn01Challenge.class).orElseThrow();\n\n            client.tlsAlpnAddCertificate(\n                        auth.getIdentifier().getDomain(),\n                        challenge.getAuthorization());\n\n            cleanup(() -> client.tlsAlpnRemoveCertificate(auth.getIdentifier().getDomain()));\n\n            return challenge;\n        }, OrderIT::standardRevoker, profile);\n    }\n\n    /**\n     * Test if a certificate can be revoked by its domain key.\n     */\n    @Test\n    public void testDomainKeyRevocation() throws Exception {\n        orderCertificate(TEST_DOMAIN, auth -> {\n            var client = getBammBammClient();\n\n            var challenge = auth.findChallenge(Http01Challenge.class).orElseThrow();\n\n            client.httpAddToken(challenge.getToken(), challenge.getAuthorization());\n\n            cleanup(() -> client.httpRemoveToken(challenge.getToken()));\n\n            return challenge;\n        }, OrderIT::domainKeyRevoker, null);\n    }\n\n    /**\n     * Runs the complete process of ordering a certificate.\n     *\n     * @param domain\n     *            Name of the domain to order a certificate for\n     * @param validator\n     *            {@link Validator} that finds and prepares a {@link Challenge} for domain\n     *            validation\n     * @param revoker\n     *            {@link Revoker} that finally revokes the certificate\n     * @param profile\n     *            Profile to be used, or {@code null} for no profile selection.\n     */\n    private void orderCertificate(String domain, Validator validator, Revoker revoker, String profile)\n            throws Exception {\n        var keyPair = createKeyPair();\n        var session = new Session(pebbleURI());\n\n        var account = new AccountBuilder()\n                    .agreeToTermsOfService()\n                    .useKeyPair(keyPair)\n                    .create(session);\n\n        var domainKeyPair = createKeyPair();\n\n        var notBefore = Instant.now().truncatedTo(ChronoUnit.SECONDS);\n        var notAfter = notBefore.plus(Duration.ofDays(20L));\n\n        var orderBuilder = account.newOrder()\n                .domain(domain)\n                .notBefore(notBefore)\n                .notAfter(notAfter);\n\n        if (profile != null) {\n            orderBuilder.profile(profile);\n        }\n\n        var order = orderBuilder.create();\n        assertThat(order.getNotBefore().orElseThrow()).isEqualTo(notBefore);\n        assertThat(order.getNotAfter().orElseThrow()).isEqualTo(notAfter);\n        assertThat(order.getStatus()).isEqualTo(Status.PENDING);\n\n        if (profile != null) {\n            assertThat(order.getProfile()).contains(profile);\n        } else {\n            // FIXME: Pebble falls back to different values here, cannot be tested properly\n        }\n\n        for (var auth : order.getAuthorizations()) {\n            assertThat(auth.getIdentifier().getDomain()).isEqualTo(domain);\n            assertThat(auth.getStatus()).isEqualTo(Status.PENDING);\n\n            if (auth.getStatus() == Status.VALID) {\n                continue;\n            }\n\n            var challenge = validator.prepare(auth);\n            challenge.trigger();\n\n            challenge.waitForCompletion(TIMEOUT);\n\n            assertThat(challenge.getStatus()).isEqualTo(Status.VALID);\n\n            auth.fetch();\n            assertThat(auth.getStatus()).isEqualTo(Status.VALID);\n        }\n\n        order.waitUntilReady(TIMEOUT);\n        assertThat(order.getStatus()).isEqualTo(Status.READY);\n\n        order.execute(domainKeyPair);\n\n        order.waitForCompletion(TIMEOUT);\n        assertThat(order.getStatus()).isEqualTo(Status.VALID);\n\n        var certificate = order.getCertificate();\n        var cert = certificate.getCertificate();\n        assertThat(cert).isNotNull();\n        assertThat(cert.getNotBefore().toInstant()).isEqualTo(notBefore);\n        assertThat(cert.getNotAfter().toInstant()).isEqualTo(notAfter);\n\n        for (var auth :  order.getAuthorizations()) {\n            assertThat(auth.getStatus()).isEqualTo(Status.VALID);\n            auth.deactivate();\n            assertThat(auth.getStatus()).isEqualTo(Status.DEACTIVATED);\n        }\n\n        revoker.revoke(session, certificate, keyPair, domainKeyPair);\n\n        var ex2 = assertThrows(AcmeServerException.class,\n                certificate::revoke,\n                \"Could revoke again\");\n        assertThat(ex2.getProblem().getType()).isEqualTo(URI.create(\"urn:ietf:params:acme:error:alreadyRevoked\"));\n    }\n\n    /**\n     * Revokes a certificate by calling {@link Certificate#revoke(RevocationReason)}.\n     * This is the standard way to revoke a certificate.\n     */\n    private static void standardRevoker(Session session, Certificate certificate,\n            KeyPair keyPair, KeyPair domainKeyPair) throws Exception {\n        certificate.revoke(RevocationReason.KEY_COMPROMISE);\n    }\n\n    /**\n     * Revokes a certificate by calling\n     * {@link Certificate#revoke(Session, KeyPair, X509Certificate, RevocationReason)}.\n     * This way can be used when the account key was lost.\n     */\n    private static void domainKeyRevoker(Session session, Certificate certificate,\n            KeyPair keyPair, KeyPair domainKeyPair) throws Exception {\n        Certificate.revoke(session, domainKeyPair, certificate.getCertificate(),\n                RevocationReason.KEY_COMPROMISE);\n    }\n\n    @FunctionalInterface\n    private interface Validator {\n        Challenge prepare(Authorization auth) throws Exception;\n    }\n\n    @FunctionalInterface\n    private interface Revoker {\n        void revoke(Session session, Certificate certificate, KeyPair keyPair,\n            KeyPair domainKeyPair) throws Exception;\n    }\n\n}\n"
  },
  {
    "path": "acme4j-it/src/test/java/org/shredzone/acme4j/it/pebble/OrderWildcardIT.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.it.pebble;\n\nimport static java.util.stream.Collectors.toList;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.temporal.ChronoUnit;\n\nimport org.bouncycastle.asn1.x509.GeneralName;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.AccountBuilder;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.challenge.Dns01Challenge;\nimport org.shredzone.acme4j.challenge.DnsPersist01Challenge;\n\n/**\n * Tests a complete wildcard certificate order. Wildcard certificates currently only\n * support dns-01 challenge.\n */\npublic class OrderWildcardIT extends PebbleITBase {\n\n    private static final String TEST_DOMAIN = \"example.com\";\n    private static final String TEST_WILDCARD_DOMAIN = \"*.example.com\";\n    private static final Duration TIMEOUT = Duration.ofSeconds(30L);\n\n    /**\n     * Test if a wildcard certificate can be ordered via dns-01 challenge.\n     */\n    @Test\n    public void testDnsValidation() throws Exception {\n        var client = getBammBammClient();\n        var keyPair = createKeyPair();\n        var session = new Session(pebbleURI());\n\n        var account = new AccountBuilder()\n                    .agreeToTermsOfService()\n                    .useKeyPair(keyPair)\n                    .create(session);\n\n        var domainKeyPair = createKeyPair();\n\n        var notBefore = Instant.now().truncatedTo(ChronoUnit.MILLIS);\n        var notAfter = notBefore.plus(Duration.ofDays(20L));\n\n        var order = account.newOrder()\n                    .domain(TEST_WILDCARD_DOMAIN)\n                    .domain(TEST_DOMAIN)\n                    .notBefore(notBefore)\n                    .notAfter(notAfter)\n                    .create();\n        assertThat(order.getNotBefore().orElseThrow()).isEqualTo(notBefore);\n        assertThat(order.getNotAfter().orElseThrow()).isEqualTo(notAfter);\n        assertThat(order.getStatus()).isEqualTo(Status.PENDING);\n\n        for (var auth : order.getAuthorizations()) {\n            assertThat(auth.getIdentifier().getDomain()).isEqualTo(TEST_DOMAIN);\n\n            if (auth.getStatus() == Status.VALID) {\n                continue;\n            }\n\n            var challenge = auth.findChallenge(Dns01Challenge.class).orElseThrow();\n\n            var challengeDomainName = challenge.getRRName(TEST_DOMAIN);\n\n            client.dnsAddTxtRecord(challengeDomainName, challenge.getDigest());\n\n            try {\n                challenge.trigger();\n                challenge.waitForCompletion(TIMEOUT);\n                assertThat(challenge.getStatus()).isEqualTo(Status.VALID);\n            } finally {\n                performCleanup();\n            }\n\n            auth.fetch();\n            assertThat(auth.getStatus()).isEqualTo(Status.VALID);\n        }\n\n        order.waitUntilReady(TIMEOUT);\n        assertThat(order.getStatus()).isEqualTo(Status.READY);\n\n        order.execute(domainKeyPair);\n        order.waitForCompletion(TIMEOUT);\n        assertThat(order.getStatus()).isEqualTo(Status.VALID);\n\n        var cert = order.getCertificate().getCertificate();\n        assertThat(cert).isNotNull();\n        assertThat(cert.getNotAfter()).isNotEqualTo(notBefore);\n        assertThat(cert.getNotBefore()).isNotEqualTo(notAfter);\n\n        var san = cert.getSubjectAlternativeNames().stream()\n                .filter(it -> ((Number) it.get(0)).intValue() == GeneralName.dNSName)\n                .map(it -> (String) it.get(1))\n                .collect(toList());\n        assertThat(san).contains(TEST_DOMAIN, TEST_WILDCARD_DOMAIN);\n    }\n\n    /**\n     * Test if a wildcard certificate can be ordered via dns-persist-01 challenge.\n     */\n    @Test\n    public void testDnsPersistValidation() throws Exception {\n        var client = getBammBammClient();\n        var keyPair = createKeyPair();\n        var session = new Session(pebbleURI());\n\n        var account = new AccountBuilder()\n                .agreeToTermsOfService()\n                .useKeyPair(keyPair)\n                .create(session);\n\n        var domainKeyPair = createKeyPair();\n\n        var notBefore = Instant.now().truncatedTo(ChronoUnit.MILLIS);\n        var notAfter = notBefore.plus(Duration.ofDays(20L));\n\n        var order = account.newOrder()\n                .domain(TEST_WILDCARD_DOMAIN)\n                .domain(TEST_DOMAIN)\n                .notBefore(notBefore)\n                .notAfter(notAfter)\n                .create();\n        assertThat(order.getNotBefore().orElseThrow()).isEqualTo(notBefore);\n        assertThat(order.getNotAfter().orElseThrow()).isEqualTo(notAfter);\n        assertThat(order.getStatus()).isEqualTo(Status.PENDING);\n\n        for (var auth : order.getAuthorizations()) {\n            assertThat(auth.getIdentifier().getDomain()).isEqualTo(TEST_DOMAIN);\n\n            if (auth.getStatus() == Status.VALID) {\n                continue;\n            }\n\n            var challenge = auth.findChallenge(DnsPersist01Challenge.class).orElseThrow();\n\n            var challengeDomainName = challenge.getRRName(TEST_DOMAIN);\n\n            client.dnsAddTxtRecord(challengeDomainName,\n                    challenge.buildRData().wildcard().noQuotes().build());\n\n            try {\n                challenge.trigger();\n                challenge.waitForCompletion(TIMEOUT);\n                assertThat(challenge.getStatus()).isEqualTo(Status.VALID);\n            } finally {\n                performCleanup();\n            }\n\n            auth.fetch();\n            assertThat(auth.getStatus()).isEqualTo(Status.VALID);\n        }\n\n        order.waitUntilReady(TIMEOUT);\n        assertThat(order.getStatus()).isEqualTo(Status.READY);\n\n        order.execute(domainKeyPair);\n        order.waitForCompletion(TIMEOUT);\n        assertThat(order.getStatus()).isEqualTo(Status.VALID);\n\n        var cert = order.getCertificate().getCertificate();\n        assertThat(cert).isNotNull();\n        assertThat(cert.getNotAfter()).isNotEqualTo(notBefore);\n        assertThat(cert.getNotBefore()).isNotEqualTo(notAfter);\n\n        var san = cert.getSubjectAlternativeNames().stream()\n                .filter(it -> ((Number) it.get(0)).intValue() == GeneralName.dNSName)\n                .map(it -> (String) it.get(1))\n                .collect(toList());\n        assertThat(san).contains(TEST_DOMAIN, TEST_WILDCARD_DOMAIN);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-it/src/test/java/org/shredzone/acme4j/it/pebble/PebbleITBase.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.it.pebble;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.net.URI;\nimport java.net.URL;\nimport java.security.KeyPair;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.shredzone.acme4j.Authorization;\nimport org.shredzone.acme4j.Order;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.exception.AcmeLazyLoadingException;\nimport org.shredzone.acme4j.it.BammBammClient;\nimport org.shredzone.acme4j.util.KeyPairUtils;\n\n/**\n * Superclass for all Pebble related integration tests.\n * <p>\n * These tests require a running\n * <a href=\"https://github.com/letsencrypt/pebble\">Pebble</a> ACME test server at\n * localhost port 14000. The host and port can be changed via the system property\n * {@code pebbleHost} and {@code pebblePort} respectively.\n * <p>\n * Also, a running pebble-challtestsrv is required to listen on localhost port 8055. The\n * server's base URL can be changed via the system property {@code bammbammUrl}.\n */\npublic abstract class PebbleITBase {\n    private final String pebbleHost = System.getProperty(\"pebbleHost\", \"localhost\");\n    private final int pebblePort = Integer.parseInt(System.getProperty(\"pebblePort\", \"14000\"));\n\n    private final String bammbammUrl = System.getProperty(\"bammbammUrl\", \"http://localhost:8055\");\n\n    private BammBammClient bammBammClient;\n\n    private final List<CleanupCallback> cleanup = new ArrayList<>();\n\n    @AfterEach\n    public void performCleanup() throws Exception {\n        for (var callback : cleanup) {\n            callback.cleanup();\n        }\n        cleanup.clear();\n    }\n\n    protected void cleanup(CleanupCallback callback) {\n        cleanup.add(callback);\n    }\n\n    /**\n     * @return The {@link URI} of the pebble server to test against.\n     */\n    protected URI pebbleURI() {\n        return URI.create(\"acme://pebble/\" + pebbleHost + \":\" + pebblePort);\n    }\n\n    /**\n     * @return {@link BammBammClient} singleton instance.\n     */\n    protected BammBammClient getBammBammClient() {\n        if (bammBammClient == null) {\n            bammBammClient = new BammBammClient(bammbammUrl);\n        }\n        return bammBammClient;\n    }\n\n    /**\n     * Creates a fresh key pair.\n     *\n     * @return Created {@link KeyPair}, guaranteed to be unknown to the Pebble server\n     */\n    protected KeyPair createKeyPair() {\n        return KeyPairUtils.createKeyPair(2048);\n    }\n\n    /**\n     * Asserts that the given {@link URL} is not {@code null} and refers to the Pebble\n     * server.\n     *\n     * @param url\n     *            {@link URL} to assert\n     */\n    protected void assertIsPebbleUrl(URL url) {\n        assertThat(url).isNotNull();\n        assertThat(url.getProtocol()).isEqualTo(\"https\");\n        assertThat(url.getHost()).isEqualTo(pebbleHost);\n        assertThat(url.getPort()).isEqualTo(pebblePort);\n        assertThat(url.getPath()).isNotEmpty();\n    }\n\n    /**\n     * Safely updates the authorization, catching checked exceptions.\n     *\n     * @param auth\n     *            {@link Authorization} to update\n     */\n    protected void updateAuth(Authorization auth) {\n        try {\n            auth.fetch();\n        } catch (AcmeException ex) {\n            throw new AcmeLazyLoadingException(auth, ex);\n        }\n    }\n\n    /**\n     * Safely updates the order, catching checked exceptions.\n     *\n     * @param order\n     *            {@link Order} to update\n     */\n    protected void updateOrder(Order order) {\n        try {\n            order.fetch();\n        } catch (AcmeException ex) {\n            throw new AcmeLazyLoadingException(order, ex);\n        }\n    }\n\n    @FunctionalInterface\n    public interface CleanupCallback {\n        void cleanup() throws Exception;\n    }\n\n}\n"
  },
  {
    "path": "acme4j-it/src/test/java/org/shredzone/acme4j/it/pebble/SessionIT.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.it.pebble;\n\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.net.URI;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Session;\nimport org.shredzone.acme4j.connector.Resource;\nimport org.shredzone.acme4j.exception.AcmeException;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * Session related integration tests.\n */\npublic class SessionIT extends PebbleITBase {\n\n    @Test\n    public void testResources() throws AcmeException {\n        var session = new Session(pebbleURI());\n\n        assertIsPebbleUrl(session.resourceUrl(Resource.NEW_ACCOUNT));\n        assertIsPebbleUrl(session.resourceUrl(Resource.NEW_NONCE));\n        assertIsPebbleUrl(session.resourceUrl(Resource.NEW_ORDER));\n    }\n\n    @Test\n    public void testMetadata() throws AcmeException {\n        var session = new Session(pebbleURI());\n\n        var meta = session.getMetadata();\n        assertThat(meta).isNotNull();\n\n        assertThat(meta.getTermsOfService().orElseThrow())\n                .isEqualTo(URI.create(\"data:text/plain,Do%20what%20thou%20wilt\"));\n        assertThat(meta.getWebsite()).isEmpty();\n        assertThat(meta.getCaaIdentities()).contains(\"pebble.letsencrypt.org\");\n        assertThat(meta.isExternalAccountRequired()).isFalse();\n        assertThat(meta.getProfiles()).contains(\"default\", \"shortlived\");\n        assertThat(meta.getProfileDescription(\"default\")).contains(\"The profile you know and love\");\n        assertThat(meta.getProfileDescription(\"shortlived\")).contains(\"A short-lived cert profile, without actual enforcement\");\n        assertThat(meta.getProfileDescription(\"paid\")).isEmpty();\n        assertThatJson(meta.getJSON().toString()).isEqualTo(JSON.parse(\"\"\"\n                        {\n                            \"caaIdentities\": [\n                                \"pebble.letsencrypt.org\"\n                            ],\n                            \"externalAccountRequired\": false,\n                            \"profiles\": {\n                                \"default\": \"The profile you know and love\",\n                                \"shortlived\": \"A short-lived cert profile, without actual enforcement\"\n                            },\n                            \"termsOfService\": \"data:text/plain,Do%20what%20thou%20wilt\"\n                        }\n                        \"\"\").toString());\n    }\n\n}\n"
  },
  {
    "path": "acme4j-it/src/test/resources/simplelogger.properties",
    "content": "\norg.slf4j.simpleLogger.log.org.shredzone.acme4j = debug\n"
  },
  {
    "path": "acme4j-smime/.gitattributes",
    "content": "*.eml text eol=crlf\n"
  },
  {
    "path": "acme4j-smime/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n *\n * acme4j - ACME Java client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n *\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.shredzone.acme4j</groupId>\n        <artifactId>acme4j</artifactId>\n        <version>5.1.1-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>acme4j-smime</artifactId>\n\n    <name>acme4j S/MIME</name>\n    <description>acme4j S/MIME extension</description>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.shredzone.acme4j</groupId>\n            <artifactId>acme4j-client</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.bouncycastle</groupId>\n            <artifactId>bcprov-jdk18on</artifactId>\n            <version>${bouncycastle.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.bouncycastle</groupId>\n            <artifactId>bcpkix-jdk18on</artifactId>\n            <version>${bouncycastle.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.bouncycastle</groupId>\n            <artifactId>bcjmail-jdk18on</artifactId>\n            <version>${bouncycastle.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <version>${slf4j.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.angus</groupId>\n            <artifactId>jakarta.mail</artifactId>\n            <version>${jakarta.mail.version}</version>\n        </dependency>\n    </dependencies>\n\n</project>\n"
  },
  {
    "path": "acme4j-smime/src/main/java/.gitignore",
    "content": ""
  },
  {
    "path": "acme4j-smime/src/main/java/module-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This module is an add-on that provides S/MIME certificate features.\n */\nmodule org.shredzone.acme4j.smime {\n    requires org.shredzone.acme4j;\n\n    requires transitive jakarta.mail;\n    requires static com.github.spotbugs.annotations;\n    requires org.bouncycastle.mail;\n    requires org.bouncycastle.pkix;\n    requires org.bouncycastle.provider;\n\n    exports org.shredzone.acme4j.smime;\n    exports org.shredzone.acme4j.smime.challenge;\n    exports org.shredzone.acme4j.smime.csr;\n    exports org.shredzone.acme4j.smime.email;\n    exports org.shredzone.acme4j.smime.exception;\n\n    provides org.shredzone.acme4j.provider.ChallengeProvider\n            with org.shredzone.acme4j.smime.challenge.EmailReply00ChallengeProvider;\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/EmailIdentifier.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime;\n\nimport java.io.Serial;\n\nimport jakarta.mail.internet.AddressException;\nimport jakarta.mail.internet.InternetAddress;\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\n\n/**\n * Represents an e-mail identifier.\n *\n * @since 2.12\n */\npublic class EmailIdentifier extends Identifier {\n    @Serial\n    private static final long serialVersionUID = -1473014167038845395L;\n\n    /**\n     * Type constant for E-Mail identifiers.\n     *\n     * @see <a href=\"https://datatracker.ietf.org/doc/html/rfc8823\">RFC 8823</a>\n     */\n    public static final String TYPE_EMAIL = \"email\";\n\n    /**\n     * Creates a new {@link EmailIdentifier}.\n     *\n     * @param value\n     *         e-mail address\n     */\n    private EmailIdentifier(String value) {\n        super(TYPE_EMAIL, value);\n    }\n\n    /**\n     * Creates a new email identifier for the given address.\n     *\n     * @param email\n     *         Email address. Must only be the address itself (without personal name).\n     * @return New {@link EmailIdentifier}\n     */\n    public static EmailIdentifier email(String email) {\n        return new EmailIdentifier(email);\n    }\n\n    /**\n     * Creates a new email identifier for the given address.\n     *\n     * @param email\n     *         Email address. Only the address itself is used. The personal name will be\n     *         ignored.\n     * @return New {@link EmailIdentifier}\n     */\n    public static EmailIdentifier email(InternetAddress email) {\n        return email(email.getAddress());\n    }\n\n    /**\n     * Returns the email address.\n     *\n     * @return {@link InternetAddress}\n     * @throws AcmeProtocolException\n     *             if this is not a valid email identifier.\n     */\n    public InternetAddress getEmailAddress() {\n        try {\n            return new InternetAddress(getValue());\n        } catch (AddressException ex) {\n            throw new AcmeProtocolException(\"bad email address\", ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/challenge/EmailReply00Challenge.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.challenge;\n\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.base64UrlEncode;\nimport static org.shredzone.acme4j.toolbox.AcmeUtils.sha256hash;\n\nimport java.io.Serial;\n\nimport jakarta.mail.internet.AddressException;\nimport jakarta.mail.internet.InternetAddress;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.challenge.TokenChallenge;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * Implements the {@value TYPE} challenge.\n *\n * @see <a href=\"https://datatracker.ietf.org/doc/html/rfc8823\">RFC 8823</a>\n * @since 2.12\n */\npublic class EmailReply00Challenge extends TokenChallenge {\n    @Serial\n    private static final long serialVersionUID = 2502329538019544794L;\n\n    /**\n     * Challenge type name: {@value}\n     */\n    public static final String TYPE = \"email-reply-00\";\n\n    private static final String KEY_FROM = \"from\";\n\n    /**\n     * Creates a new generic {@link EmailReply00Challenge} object.\n     *\n     * @param login\n     *            {@link Login} the resource is bound with\n     * @param data\n     *            {@link JSON} challenge data\n     */\n    public EmailReply00Challenge(Login login, JSON data) {\n        super(login, data);\n    }\n\n    /**\n     * Returns the email address in the \"from\" field of the challenge.\n     *\n     * @return The \"from\" email address, as String.\n     */\n    public String getFrom() {\n        return getJSON().get(KEY_FROM).asString();\n    }\n\n    /**\n     * Returns the email address of the expected sender of the \"challenge\" mail.\n     * <p>\n     * This is the same value that is returned by {@link #getFrom()}, but as {@link\n     * InternetAddress} instance.\n     *\n     * @return Expected sender of the challenge email.\n     */\n    public InternetAddress getExpectedSender() {\n        try {\n            return new InternetAddress(getFrom());\n        } catch (AddressException ex) {\n            throw new AcmeProtocolException(\"bad email address \" + getFrom(), ex);\n        }\n    }\n\n    /**\n     * Returns the token, which is a concatenation of the part 1 that is sent by email,\n     * and part 2 that is passed into this callenge via {@link #getTokenPart2()};\n     *\n     * @param part1\n     *         Part 1 of the token, which can be found in the subject of the corresponding\n     *         challenge email.\n     * @return Concatenated token\n     */\n    public String getToken(String part1) {\n        return part1.concat(getTokenPart2());\n    }\n\n    /**\n     * Returns the part 2 of the token to be used for this challenge. Part 2 is sent via\n     * this challenge.\n     */\n    public String getTokenPart2() {\n        return super.getToken();\n    }\n\n    /**\n     * This method is not implemented. Use {@link #getAuthorization(String)} instead.\n     */\n    @Override\n    public String getAuthorization() {\n        throw new UnsupportedOperationException(\"use getAuthorization(String)\");\n    }\n\n    /**\n     * Returns the authorization string.\n     *\n     * @param part1\n     *         Part 1 of the token, which can be found in the subject of the corresponding\n     *         challenge email.\n     */\n    public String getAuthorization(String part1) {\n        var keyAuth = keyAuthorizationFor(getToken(part1));\n        return base64UrlEncode(sha256hash(keyAuth));\n    }\n\n    @Override\n    protected boolean acceptable(String type) {\n        return TYPE.equals(type);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/challenge/EmailReply00ChallengeProvider.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.challenge;\n\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.challenge.Challenge;\nimport org.shredzone.acme4j.provider.ChallengeProvider;\nimport org.shredzone.acme4j.provider.ChallengeType;\nimport org.shredzone.acme4j.toolbox.JSON;\n\n/**\n * A provider that generates {@link EmailReply00Challenge}. It is registered as Java\n * service.\n *\n * @since 2.12\n */\n@ChallengeType(EmailReply00Challenge.TYPE)\npublic class EmailReply00ChallengeProvider implements ChallengeProvider {\n\n    @Override\n    public Challenge create(Login login, JSON data) {\n        return new EmailReply00Challenge(login, data);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/challenge/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains the\n * {@link org.shredzone.acme4j.smime.challenge.EmailReply00Challenge#TYPE} related acme4j\n * {@link org.shredzone.acme4j.challenge.Challenge} implementation.\n * <p>\n * The {@link org.shredzone.acme4j.smime.challenge.EmailReply00ChallengeProvider} is\n * registered as Java service, so acme4j is able to automatically generate\n * {@link org.shredzone.acme4j.smime.challenge.EmailReply00Challenge} instances.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.smime.challenge;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/csr/KeyUsageType.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.csr;\n\nimport org.bouncycastle.asn1.x509.KeyUsage;\n\n/**\n * An enumeration of key usage types for S/MIME certificates.\n *\n * @since 2.12\n */\npublic enum KeyUsageType {\n\n    /**\n     * S/MIME certificate can be used only for signing.\n     */\n    SIGNING_ONLY(KeyUsage.digitalSignature),\n\n    /**\n     * S/MIME certificate can be used only for encryption.\n     */\n    ENCRYPTION_ONLY(KeyUsage.keyEncipherment),\n\n    /**\n     * S/MIME certificate can be used for both signing and encryption.\n     */\n    SIGNING_AND_ENCRYPTION(KeyUsage.digitalSignature | KeyUsage.keyEncipherment);\n\n    private final int keyUsage;\n\n    KeyUsageType(int keyUsage) {\n        this.keyUsage = keyUsage;\n    }\n\n    /**\n     * Returns the key usage bits to be used in the key usage extension of a CSR.\n     */\n    public int getKeyUsageBits() {\n        return keyUsage;\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/csr/SMIMECSRBuilder.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.csr;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static java.util.Objects.requireNonNull;\nimport static java.util.stream.Collectors.joining;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.Writer;\nimport java.security.KeyPair;\nimport java.security.interfaces.ECKey;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport jakarta.mail.internet.AddressException;\nimport jakarta.mail.internet.InternetAddress;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;\nimport org.bouncycastle.asn1.x500.X500Name;\nimport org.bouncycastle.asn1.x500.X500NameBuilder;\nimport org.bouncycastle.asn1.x500.style.BCStyle;\nimport org.bouncycastle.asn1.x509.Extension;\nimport org.bouncycastle.asn1.x509.ExtensionsGenerator;\nimport org.bouncycastle.asn1.x509.GeneralName;\nimport org.bouncycastle.asn1.x509.GeneralNames;\nimport org.bouncycastle.asn1.x509.KeyUsage;\nimport org.bouncycastle.operator.OperatorCreationException;\nimport org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;\nimport org.bouncycastle.pkcs.PKCS10CertificationRequest;\nimport org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;\nimport org.bouncycastle.util.io.pem.PemObject;\nimport org.bouncycastle.util.io.pem.PemWriter;\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.smime.EmailIdentifier;\n\n/**\n * Generator for an S/MIME CSR (Certificate Signing Request) suitable for ACME servers.\n * <p>\n * Requires {@code Bouncy Castle}. The {@link org.bouncycastle.jce.provider.BouncyCastleProvider}\n * must also be added as security provider.\n * <p>\n * A {@code jakarta.mail} implementation must be present in the classpath.\n *\n * @since 2.12\n */\npublic class SMIMECSRBuilder {\n    private static final String SIGNATURE_ALG = \"SHA256withRSA\";\n    private static final String EC_SIGNATURE_ALG = \"SHA256withECDSA\";\n\n    private final X500NameBuilder namebuilder = new X500NameBuilder(X500Name.getDefaultStyle());\n    private final List<InternetAddress> emaillist = new ArrayList<>();\n    private @Nullable PKCS10CertificationRequest csr = null;\n    private KeyUsageType keyUsageType = KeyUsageType.SIGNING_AND_ENCRYPTION;\n\n    /**\n     * Adds an {@link InternetAddress}. The first address is also used as CN.\n     *\n     * @param email\n     *            {@link InternetAddress} to add\n     */\n    public void addEmail(InternetAddress email) {\n        if (emaillist.isEmpty()) {\n            namebuilder.addRDN(BCStyle.CN, email.getAddress());\n        }\n        emaillist.add(email);\n    }\n\n    /**\n     * Adds multiple {@link InternetAddress}.\n     *\n     * @param emails\n     *            Collection of {@link InternetAddress} to add\n     */\n    public void addEmails(Collection<InternetAddress> emails) {\n        emails.forEach(this::addEmail);\n    }\n\n    /**\n     * Adds multiple {@link InternetAddress}.\n     *\n     * @param emails\n     *            {@link InternetAddress} to add\n     */\n    public void addEmails(InternetAddress... emails) {\n        Arrays.stream(emails).forEach(this::addEmail);\n    }\n\n    /**\n     * Adds an email {@link Identifier}.\n     *\n     * @param id\n     *            {@link Identifier} to add\n     */\n    public void addIdentifier(Identifier id) {\n        requireNonNull(id);\n        if (!EmailIdentifier.TYPE_EMAIL.equals(id.getType())) {\n            throw new AcmeProtocolException(\"Expected type email, but got \" + id.getType());\n        }\n\n        try {\n            addEmail(new InternetAddress(id.getValue()));\n        } catch (AddressException ex) {\n            throw new AcmeProtocolException(\"bad email address\", ex);\n        }\n    }\n\n    /**\n     * Adds a {@link Collection} of email {@link Identifier}.\n     *\n     * @param ids\n     *            Collection of Identifier to add\n     */\n    public void addIdentifiers(Collection<Identifier> ids) {\n        ids.forEach(this::addIdentifier);\n    }\n\n    /**\n     * Adds multiple email {@link Identifier}.\n     *\n     * @param ids\n     *            Identifier to add\n     */\n    public void addIdentifiers(Identifier... ids) {\n        Arrays.stream(ids).forEach(this::addIdentifier);\n    }\n\n    /**\n     * Sets an entry of the subject used for the CSR.\n     * <p>\n     * This method is meant as \"expert mode\" for setting attributes that are not covered\n     * by the other methods. It is at the discretion of the ACME server to accept this\n     * parameter.\n     *\n     * @param attName\n     *         The BCStyle attribute name\n     * @param value\n     *         The value\n     * @throws AddressException\n     *         if a common name is added, but the value is not a valid email address.\n     * @since 2.14\n     */\n    public void addValue(String attName, String value) throws AddressException {\n        var oid = X500Name.getDefaultStyle().attrNameToOID(requireNonNull(attName, \"attribute name must not be null\"));\n        addValue(oid, value);\n    }\n\n    /**\n     * Sets an entry of the subject used for the CSR\n     * <p>\n     * This method is meant as \"expert mode\" for setting attributes that are not covered\n     * by the other methods. It is at the discretion of the ACME server to accept this\n     * parameter.\n     *\n     * @param oid\n     *         The OID of the attribute to be added\n     * @param value\n     *         The value\n     * @throws AddressException\n     *         if a common name is added, but the value is not a valid email address.\n     * @since 2.14\n     */\n    public void addValue(ASN1ObjectIdentifier oid, String value) throws AddressException {\n        if (requireNonNull(oid, \"OID must not be null\").equals(BCStyle.CN)) {\n            addEmail(new InternetAddress(value));\n            return;\n        }\n        namebuilder.addRDN(oid, requireNonNull(value, \"attribute value must not be null\"));\n    }\n\n    /**\n     * Sets the organization.\n     * <p>\n     * Note that it is at the discretion of the ACME server to accept this parameter.\n     */\n    public void setOrganization(String o) {\n        namebuilder.addRDN(BCStyle.O, requireNonNull(o));\n    }\n\n    /**\n     * Sets the organizational unit.\n     * <p>\n     * Note that it is at the discretion of the ACME server to accept this parameter.\n     */\n    public void setOrganizationalUnit(String ou) {\n        namebuilder.addRDN(BCStyle.OU, requireNonNull(ou));\n    }\n\n    /**\n     * Sets the city or locality.\n     * <p>\n     * Note that it is at the discretion of the ACME server to accept this parameter.\n     */\n    public void setLocality(String l) {\n        namebuilder.addRDN(BCStyle.L, requireNonNull(l));\n    }\n\n    /**\n     * Sets the state or province.\n     * <p>\n     * Note that it is at the discretion of the ACME server to accept this parameter.\n     */\n    public void setState(String st) {\n        namebuilder.addRDN(BCStyle.ST, requireNonNull(st));\n    }\n\n    /**\n     * Sets the country.\n     * <p>\n     * Note that it is at the discretion of the ACME server to accept this parameter.\n     */\n    public void setCountry(String c) {\n        namebuilder.addRDN(BCStyle.C, requireNonNull(c));\n    }\n\n    /**\n     * Sets the key usage type for S/MIME certificates.\n     * <p>\n     * By default, the S/MIME certificate will be suitable for both signing and\n     * encryption.\n     */\n    public void setKeyUsageType(KeyUsageType keyUsageType) {\n        requireNonNull(keyUsageType, \"keyUsageType\");\n        this.keyUsageType = keyUsageType;\n    }\n\n    /**\n     * Signs the completed S/MIME CSR.\n     *\n     * @param keypair\n     *            {@link KeyPair} to sign the CSR with\n     */\n    public void sign(KeyPair keypair) throws IOException {\n        requireNonNull(keypair, \"keypair\");\n        if (emaillist.isEmpty()) {\n            throw new IllegalStateException(\"No email address was set\");\n        }\n\n        try {\n            var ix = 0;\n            var gns = new GeneralName[emaillist.size()];\n            for (var email : emaillist) {\n                gns[ix++] = new GeneralName(GeneralName.rfc822Name, email.getAddress());\n            }\n            var subjectAltName = new GeneralNames(gns);\n\n            var p10Builder = new JcaPKCS10CertificationRequestBuilder(namebuilder.build(), keypair.getPublic());\n\n            var extensionsGenerator = new ExtensionsGenerator();\n            extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, subjectAltName);\n\n            var keyUsage = new KeyUsage(keyUsageType.getKeyUsageBits());\n            extensionsGenerator.addExtension(Extension.keyUsage, true, keyUsage);\n\n            p10Builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate());\n\n            var pk = keypair.getPrivate();\n            var csBuilder = new JcaContentSignerBuilder(pk instanceof ECKey ? EC_SIGNATURE_ALG : SIGNATURE_ALG);\n            var signer = csBuilder.build(pk);\n\n            csr = p10Builder.build(signer);\n        } catch (OperatorCreationException ex) {\n            throw new IOException(\"Could not generate CSR\", ex);\n        }\n    }\n\n    /**\n     * Gets the PKCS#10 certification request.\n     */\n    public PKCS10CertificationRequest getCSR() {\n        if (csr == null) {\n            throw new IllegalStateException(\"sign CSR first\");\n        }\n\n        return csr;\n    }\n\n    /**\n     * Gets an encoded PKCS#10 certification request.\n     */\n    public byte[] getEncoded() throws IOException {\n        return getCSR().getEncoded();\n    }\n\n    /**\n     * Writes the signed certificate request to a {@link Writer}.\n     *\n     * @param w\n     *            {@link Writer} to write the PEM file to. The {@link Writer} is closed\n     *            after use.\n     */\n    public void write(Writer w) throws IOException {\n        if (csr == null) {\n            throw new IllegalStateException(\"sign CSR first\");\n        }\n\n        try (var pw = new PemWriter(w)) {\n            pw.writeObject(new PemObject(\"CERTIFICATE REQUEST\", getEncoded()));\n        }\n    }\n\n    /**\n     * Writes the signed certificate request to an {@link OutputStream}.\n     *\n     * @param out\n     *            {@link OutputStream} to write the PEM file to. The {@link OutputStream}\n     *            is closed after use.\n     */\n    public void write(OutputStream out) throws IOException {\n        write(new OutputStreamWriter(out, UTF_8));\n    }\n\n    @Override\n    public String toString() {\n        var sb = new StringBuilder();\n        sb.append(namebuilder.build());\n        if (!emaillist.isEmpty()) {\n            sb.append(emaillist.stream()\n                    .map(InternetAddress::getAddress)\n                    .collect(joining(\",EMAIL=\", \",EMAIL=\", \"\")));\n        }\n        sb.append(\",TYPE=\").append(keyUsageType);\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/csr/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains S/MIME CSR related utility classes.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.smime.csr;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/EmailProcessor.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.email;\n\nimport static java.util.Collections.unmodifiableCollection;\nimport static java.util.Objects.requireNonNull;\n\nimport java.net.URL;\nimport java.security.InvalidAlgorithmParameterException;\nimport java.security.KeyStore;\nimport java.security.KeyStoreException;\nimport java.security.cert.PKIXParameters;\nimport java.security.cert.X509Certificate;\nimport java.util.Collection;\nimport java.util.Optional;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.regex.Pattern;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport jakarta.mail.Message;\nimport jakarta.mail.MessagingException;\nimport jakarta.mail.Session;\nimport jakarta.mail.internet.InternetAddress;\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.smime.challenge.EmailReply00Challenge;\nimport org.shredzone.acme4j.smime.exception.AcmeInvalidMessageException;\nimport org.shredzone.acme4j.smime.wrapper.Mail;\nimport org.shredzone.acme4j.smime.wrapper.SignedMailBuilder;\nimport org.shredzone.acme4j.smime.wrapper.SimpleMail;\n\n/**\n * A processor for incoming \"Challenge\" emails.\n *\n * @see <a href=\"https://datatracker.ietf.org/doc/html/rfc8823\">RFC 8823</a>\n * @since 2.12\n */\npublic final class EmailProcessor {\n    private static final Pattern SUBJECT_PATTERN = Pattern.compile(\"ACME:\\\\s+([0-9A-Za-z_\\\\s-]+=?)\\\\s*\");\n\n    private final InternetAddress sender;\n    private final InternetAddress recipient;\n    private final @Nullable String messageId;\n    private final Collection<InternetAddress> replyTo;\n    private final String token1;\n    private final AtomicReference<EmailReply00Challenge> challengeRef = new AtomicReference<>();\n\n    /**\n     * Processes the given plain e-mail message.\n     * <p>\n     * Note that according to RFC-8823, the challenge message must be signed using either\n     * DKIM or S/MIME. This method does not do any DKIM or S/MIME validation, and assumes\n     * that this has already been done in a previous stage.\n     *\n     * @param message\n     *         E-mail that was received from the CA. The inbound MTA has already taken\n     *         care of DKIM and/or S/MIME validation.\n     * @return EmailProcessor for this e-mail\n     * @throws AcmeInvalidMessageException\n     *         if a validation failed, and the message <em>must</em> be rejected.\n     * @since 2.15\n     */\n    public static EmailProcessor plainMessage(Message message)\n            throws AcmeInvalidMessageException {\n        return builder().skipVerification().build(message);\n    }\n\n    /**\n     * Processes the given signed e-mail message.\n     * <p>\n     * This method expects an S/MIME signed message. The signature must use a certificate\n     * that can be validated using Java's cacert truststore. Strict validation rules are\n     * applied.\n     * <p>\n     * Use the {@link #builder()} method if you need to configure the validation process.\n     *\n     * @param message\n     *         S/MIME signed e-mail that was received from the CA.\n     * @return EmailProcessor for this e-mail\n     * @throws AcmeInvalidMessageException\n     *         if a validation failed, and the message <em>must</em> be rejected.\n     * @since 2.16\n     */\n    public static EmailProcessor signedMessage(Message message)\n            throws AcmeInvalidMessageException {\n        return builder().build(message);\n    }\n\n    /**\n     * Creates a {@link Builder} for building an {@link EmailProcessor} with individual\n     * configuration.\n     *\n     * @since 2.16\n     */\n    public static Builder builder() {\n        return new Builder();\n    }\n\n    /**\n     * Creates a new {@link EmailProcessor} for the incoming \"Challenge\" message.\n     * <p>\n     * The incoming message is validated against the requirements of RFC-8823.\n     *\n     * @param message\n     *         \"Challenge\" message as it was sent by the CA.\n     * @throws AcmeInvalidMessageException\n     *         if a validation failed, and the message <em>must</em> be rejected.\n     */\n    private EmailProcessor(Mail message) throws AcmeInvalidMessageException {\n        if (!message.isAutoSubmitted()) {\n            throw new AcmeInvalidMessageException(\"Message is not auto-generated\");\n        }\n\n        var subject = message.getSubject();\n        var m = SUBJECT_PATTERN.matcher(subject);\n        if (!m.matches()) {\n            throw new AcmeProtocolException(\"Invalid subject: \" + subject);\n        }\n        // white spaces within the token part must be ignored\n        this.token1 = m.group(1).replaceAll(\"\\\\s+\", \"\");\n\n        this.sender = message.getFrom();\n        this.recipient = message.getTo();\n        this.messageId = message.getMessageId().orElse(null);\n        this.replyTo = message.getReplyTo();\n    }\n\n    /**\n     * The expected sender of the \"challenge\" email.\n     * <p>\n     * The sender is usually checked when the {@link EmailReply00Challenge} is passed into\n     * the processor, but you can also manually check the sender here.\n     *\n     * @param expectedSender\n     *         The expected sender of the \"challenge\" email.\n     * @return itself\n     * @throws AcmeProtocolException\n     *         if the expected sender does not match\n     */\n    public EmailProcessor expectedFrom(InternetAddress expectedSender) {\n        requireNonNull(expectedSender, \"expectedSender\");\n        if (!sender.equals(expectedSender)) {\n            throw new AcmeProtocolException(\"Message is not sent by the expected sender\");\n        }\n        return this;\n    }\n\n    /**\n     * The expected recipient of the \"challenge\" email.\n     * <p>\n     * This must be the email address of the entity that requested the S/MIME certificate.\n     * The check is not performed by the processor, but <em>should</em> be performed by\n     * the client.\n     *\n     * @param expectedRecipient\n     *         The expected recipient of the \"challenge\" email.\n     * @return itself\n     * @throws AcmeProtocolException\n     *         if the expected recipient does not match\n     */\n    public EmailProcessor expectedTo(InternetAddress expectedRecipient) {\n        requireNonNull(expectedRecipient, \"expectedRecipient\");\n        if (!recipient.equals(expectedRecipient)) {\n            throw new AcmeProtocolException(\"Message is not addressed to expected recipient\");\n        }\n        return this;\n    }\n\n    /**\n     * The expected identifier.\n     * <p>\n     * This must be the email address of the entity that requested the S/MIME certificate.\n     * The check is not performed by the processor, but <em>should</em> be performed by\n     * the client.\n     *\n     * @param expectedIdentifier\n     *         The expected identifier for the S/MIME certificate. Usually this is an\n     *         {@link org.shredzone.acme4j.smime.EmailIdentifier} instance.\n     * @return itself\n     * @throws AcmeProtocolException\n     *         if the expected identifier is not an email identifier, or does not match\n     */\n    public EmailProcessor expectedIdentifier(Identifier expectedIdentifier) {\n        requireNonNull(expectedIdentifier, \"expectedIdentifier\");\n        if (!\"email\".equals(expectedIdentifier.getType())) {\n            throw new AcmeProtocolException(\"Wrong identifier type: \" + expectedIdentifier.getType());\n        }\n        try {\n            expectedTo(new InternetAddress(expectedIdentifier.getValue()));\n        } catch (MessagingException ex) {\n            throw new AcmeProtocolException(\"Invalid email address\", ex);\n        }\n        return this;\n    }\n\n    /**\n     * Returns the sender of the \"challenge\" email.\n     */\n    public InternetAddress getSender() {\n        return (InternetAddress) sender.clone();\n    }\n\n    /**\n     * Returns the recipient of the \"challenge\" email.\n     */\n    public InternetAddress getRecipient() {\n        return (InternetAddress) recipient.clone();\n    }\n\n    /**\n     * Returns all \"reply-to\" email addresses found in the \"challenge\" email.\n     * <p>\n     * Empty if there was no reply-to header, but never {@code null}.\n     */\n    public Collection<InternetAddress> getReplyTo() {\n        return unmodifiableCollection(replyTo);\n    }\n\n    /**\n     * Returns the message-id of the \"challenge\" email.\n     * <p>\n     * Empty if the challenge email has no message-id.\n     */\n    public Optional<String> getMessageId() {\n        return Optional.ofNullable(messageId);\n    }\n\n    /**\n     * Returns the \"token 1\" found in the subject of the \"challenge\" email.\n     */\n    public String getToken1() {\n        return token1;\n    }\n\n    /**\n     * Sets the corresponding {@link EmailReply00Challenge} that was received from the CA\n     * for validation.\n     *\n     * @param challenge\n     *         {@link EmailReply00Challenge} that corresponds to this email\n     * @return itself\n     * @throws AcmeProtocolException\n     *         if the challenge does not match this \"challenge\" email.\n     */\n    public EmailProcessor withChallenge(EmailReply00Challenge challenge) {\n        requireNonNull(challenge, \"challenge\");\n        expectedFrom(challenge.getExpectedSender());\n        if (challengeRef.get() != null) {\n            throw new IllegalStateException(\"A challenge has already been set\");\n        }\n        challengeRef.set(challenge);\n        return this;\n    }\n\n    /**\n     * Sets the corresponding {@link EmailReply00Challenge} that was received from the CA\n     * for validation.\n     * <p>\n     * This is a convenience call in case that only the challenge location URL is\n     * available.\n     *\n     * @param login\n     *         A valid {@link Login}\n     * @param challengeLocation\n     *         The location URL of the corresponding challenge.\n     * @return itself\n     * @throws AcmeProtocolException\n     *         if the challenge does not match this \"challenge\" email.\n     */\n    public EmailProcessor withChallenge(Login login, URL challengeLocation) {\n        return withChallenge(login.bindChallenge(challengeLocation, EmailReply00Challenge.class));\n    }\n\n    /**\n     * Returns the full token of this challenge.\n     * <p>\n     * The corresponding email-reply-00 challenge must be set before.\n     */\n    public String getToken() {\n        checkChallengePresent();\n        return challengeRef.get().getToken(getToken1());\n    }\n\n    /**\n     * Returns the key-authorization of this challenge. This is the response to be used in\n     * the response email.\n     * <p>\n     * The corresponding email-reply-00 challenge must be set before.\n     */\n    public String getAuthorization() {\n        checkChallengePresent();\n        return challengeRef.get().getAuthorization(getToken1());\n    }\n\n    /**\n     * Returns a {@link ResponseGenerator} for generating a response email.\n     * <p>\n     * The corresponding email-reply-00 challenge must be set before.\n     */\n    public ResponseGenerator respond() {\n        checkChallengePresent();\n        return new ResponseGenerator(this);\n    }\n\n    /**\n     * Checks if a challenge has been set. Throws an exception if not.\n     */\n    private void checkChallengePresent() {\n        if (challengeRef.get() == null) {\n            throw new IllegalStateException(\"No challenge has been set yet\");\n        }\n    }\n\n    /**\n     * A builder for {@link EmailProcessor}.\n     * <p>\n     * Use {@link EmailProcessor#builder()} to generate an instance.\n     *\n     * @since 2.16\n     */\n    public static class Builder {\n        private boolean unsigned = false;\n        private final SignedMailBuilder builder = new SignedMailBuilder();\n\n        private Builder() {\n            // Private constructor\n        }\n\n        /**\n         * Skips signature and header verification. Use only if the message has already\n         * been verified in a previous stage (e.g. by the MTA) or for testing purposes.\n         */\n        public Builder skipVerification() {\n            this.unsigned = true;\n            return this;\n        }\n\n        /**\n         * Uses the standard cacerts truststore for signature verification. This is the\n         * default.\n         */\n        public Builder caCerts() {\n            builder.withCaCertsTrustStore();\n            return this;\n        }\n\n        /**\n         * Uses the given truststore for signature verification.\n         * <p>\n         * This is for self-signed certificates. No revocation checks will take place.\n         *\n         * @param trustStore\n         *         {@link KeyStore} of the truststore to be used.\n         */\n        public Builder trustStore(KeyStore trustStore) {\n            try {\n                builder.withTrustStore(trustStore);\n            } catch (KeyStoreException | InvalidAlgorithmParameterException ex) {\n                throw new IllegalArgumentException(\"Cannot use trustStore\", ex);\n            }\n            return this;\n        }\n\n        /**\n         * Uses the given certificate for signature verification.\n         * <p>\n         * This is for self-signed certificates. No revocation checks will take place.\n         *\n         * @param certificate\n         *         {@link X509Certificate} of the CA\n         */\n        public Builder certificate(X509Certificate certificate) {\n            builder.withSignCert(certificate);\n            return this;\n        }\n\n        /**\n         * Uses the given {@link PKIXParameters}.\n         *\n         * @param param\n         *         {@link PKIXParameters} to be used for signature verification.\n         */\n        public Builder pkixParameters(PKIXParameters param) {\n            builder.withPKIXParameters(param);\n            return this;\n        }\n\n        /**\n         * Uses the given mail {@link Session} for accessing the signed message body. A\n         * simple default session is used otherwise, which is usually sufficient.\n         *\n         * @param session\n         *         {@link Session} to be used for accessing the message body.\n         */\n        public Builder mailSession(Session session) {\n            builder.withMailSession(session);\n            return this;\n        }\n\n        /**\n         * Performs strict checks. Secured headers must exactly match their unsecured\n         * counterparts. This is the default.\n         */\n        public Builder strict() {\n            builder.relaxed(false);\n            return this;\n        }\n\n        /**\n         * Performs relaxed checks. Secured headers might differ in whitespaces or case of\n         * the field names. Use this if your MTA has mangled the envelope header.\n         */\n        public Builder relaxed() {\n            builder.relaxed(true);\n            return this;\n        }\n\n        /**\n         * Builds an {@link EmailProcessor} for the given {@link Message} using the\n         * current configuration.\n         *\n         * @param message\n         *         {@link Message} to create an {@link EmailProcessor} for.\n         * @return The generated {@link EmailProcessor}\n         * @throws AcmeInvalidMessageException\n         *         if the message fails to be verified. If this exception is thrown, the\n         *         message MUST be rejected, and MUST NOT be used for certification.\n         */\n        public EmailProcessor build(Message message) throws AcmeInvalidMessageException {\n            if (unsigned) {\n                return new EmailProcessor(new SimpleMail(message));\n            } else {\n                return new EmailProcessor(builder.build(message));\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/ResponseBodyGenerator.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.email;\n\nimport jakarta.mail.Message;\nimport jakarta.mail.MessagingException;\n\n/**\n * A generator for the response body to be set to the {@link Message}.\n * <p>\n * This generator can be used to design the body of the outgoing response email. However,\n * note that the response email is evaluated by a machine and usually not read by humans,\n * so the design should be kept simple, and <em>must</em> be conformous to RFC-8823.\n * <p>\n * The {@code responseBody} must be a part of the response email body, otherwise the\n * validation will fail.\n * <p>\n * A minimal implementation is:\n * <pre>\n * response.setContent(responseBody, RESPONSE_BODY_TYPE);\n * </pre>\n *\n * @see <a href=\"https://datatracker.ietf.org/doc/html/rfc8823\">RFC 8823</a>\n * @since 2.12\n */\n@FunctionalInterface\npublic interface ResponseBodyGenerator {\n\n    /**\n     * The content-type of the response body: {@value #RESPONSE_BODY_TYPE}\n     */\n    String RESPONSE_BODY_TYPE = \"text/plain\";\n\n    /**\n     * Sets the content of the {@link Message}.\n     *\n     * @param response\n     *         {@link Message} to set the body content.\n     * @param responseBody\n     *         The response body that <em>must</em> be part of the email response, and\n     *         <em>must</em> use {@value #RESPONSE_BODY_TYPE} content type.\n     */\n    void setContent(Message response, String responseBody) throws MessagingException;\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/ResponseGenerator.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.email;\n\nimport static java.util.Objects.requireNonNull;\nimport static jakarta.mail.Message.RecipientType.TO;\nimport static org.shredzone.acme4j.smime.email.ResponseBodyGenerator.RESPONSE_BODY_TYPE;\n\nimport java.util.Properties;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport jakarta.mail.Address;\nimport jakarta.mail.Message;\nimport jakarta.mail.MessagingException;\nimport jakarta.mail.Session;\nimport jakarta.mail.internet.MimeMessage;\n\n/**\n * A helper for creating an email response to the \"challenge\" email.\n * <p>\n * According to RFC-8823, the response email <em>must</em> be DKIM signed. This is\n * <em>not</em> done by the response generator, but must be done by the outbound MTA.\n *\n * @see <a href=\"https://datatracker.ietf.org/doc/html/rfc8823\">RFC 8823</a>\n * @since 2.12\n */\npublic class ResponseGenerator {\n    private static final int LINE_LENGTH = 72;\n    private static final String CRLF = \"\\r\\n\";\n\n    private final EmailProcessor processor;\n    private ResponseBodyGenerator generator = this::defaultBodyGenerator;\n    private @Nullable String header;\n    private @Nullable String footer;\n\n    /**\n     * Creates a new {@link ResponseGenerator}.\n     *\n     * @param processor\n     *         {@link EmailProcessor} of the challenge email.\n     */\n    public ResponseGenerator(EmailProcessor processor) {\n        this.processor = requireNonNull(processor, \"processor\");\n    }\n\n    /**\n     * Adds a custom header to the response mail body.\n     * <p>\n     * There is no need to set a header, since the response email is usually not read by\n     * humans. If a header is set, it must contain ASCII encoded plain text.\n     *\n     * @param header\n     *         Header text to be used, or {@code null} if no header is to be used.\n     * @return itself\n     */\n    public ResponseGenerator withHeader(@Nullable String header) {\n        if (header != null && !header.endsWith(CRLF)) {\n            this.header = header.concat(CRLF);\n        } else {\n            this.header = header;\n        }\n        return this;\n    }\n\n    /**\n     * Adds a custom footer to the response mail body.\n     * <p>\n     * There is no need to set a footer, since the response email is usually not read by\n     * humans. If a footer is set, it must contain ASCII encoded plain text.\n     *\n     * @param footer\n     *         Footer text to be used, or {@code null} if no footer is to be used.\n     * @return itself\n     */\n    public ResponseGenerator withFooter(@Nullable String footer) {\n        this.footer = footer;\n        return this;\n    }\n\n    /**\n     * Sets a {@link ResponseBodyGenerator} that is used for generating a response body.\n     * <p>\n     * Use this generator to individually style the email body, for example to use a\n     * multipart body. However, be aware that the response mail is evaluated by a machine,\n     * and usually not read by humans, so the body should be designed as simple as\n     * possible.\n     * <p>\n     * The default body generator will just concatenate the header, the armored key\n     * authorization body, and the footer.\n     *\n     * @param generator\n     *         {@link ResponseBodyGenerator} to be used, or {@code null} to use the\n     *         default one.\n     * @return itself\n     */\n    public ResponseGenerator withGenerator(@Nullable ResponseBodyGenerator generator) {\n        this.generator = generator != null ? generator : this::defaultBodyGenerator;\n        return this;\n    }\n\n    /**\n     * Generates the response email.\n     * <p>\n     * A simple default mail session is used for generation.\n     *\n     * @return Generated {@link Message}.\n     * @since 2.16\n     */\n    public Message generateResponse() throws MessagingException {\n        return generateResponse(Session.getDefaultInstance(new Properties()));\n    }\n\n    /**\n     * Generates the response email.\n     * <p>\n     * Note that according to RFC-8823, this message must have a valid DKIM or S/MIME\n     * signature. This is <em>not</em> done here, but usually performed by the outbound\n     * MTA.\n     *\n     * @param session\n     *         {@code jakarta.mail} {@link Session} to be used for this mail.\n     * @return Generated {@link Message}.\n     */\n    public Message generateResponse(Session session) throws MessagingException {\n        var response = new MimeMessage(requireNonNull(session, \"session\"));\n\n        response.setSubject(\"Re: ACME: \" + processor.getToken1());\n        response.setFrom(processor.getRecipient());\n\n        if (!processor.getReplyTo().isEmpty()) {\n            for (var rto : processor.getReplyTo()) {\n                response.addRecipient(TO, rto);\n            }\n        } else {\n            response.addRecipients(TO, new Address[] {processor.getSender()});\n        }\n\n        if (processor.getMessageId().isPresent()) {\n            response.setHeader(\"In-Reply-To\", processor.getMessageId().get());\n        }\n\n        var wrappedAuth = processor.getAuthorization()\n                .replaceAll(\"(.{\" + LINE_LENGTH + \"})\", \"$1\" + CRLF);\n        var responseBody = new StringBuilder();\n        responseBody.append(\"-----BEGIN ACME RESPONSE-----\").append(CRLF);\n        responseBody.append(wrappedAuth);\n        if (!wrappedAuth.endsWith(CRLF)) {\n            responseBody.append(CRLF);\n        }\n        responseBody.append(\"-----END ACME RESPONSE-----\").append(CRLF);\n\n        generator.setContent(response, responseBody.toString());\n        return response;\n    }\n\n    /**\n     * The default body generator. It just sets the response body, optionally framed by\n     * the given header and footer.\n     *\n     * @param response\n     *         response {@link Message} to fill.\n     * @param responseBody\n     *         Response body that must be added to the message.\n     */\n    private void defaultBodyGenerator(Message response, String responseBody)\n            throws MessagingException {\n        var body = new StringBuilder();\n        if (header != null) {\n            body.append(header);\n        }\n        body.append(responseBody);\n        if (footer != null) {\n            body.append(footer);\n        }\n        response.setContent(body.toString(), RESPONSE_BODY_TYPE);\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains classes for processing incoming validation emails, and for\n * generating outgoing response emails.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.smime.email;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/exception/AcmeInvalidMessageException.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2022 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.exception;\n\nimport static java.util.Collections.unmodifiableList;\n\nimport java.io.Serial;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Optional;\n\nimport org.bouncycastle.i18n.ErrorBundle;\nimport org.bouncycastle.i18n.LocalizedException;\nimport org.shredzone.acme4j.exception.AcmeException;\n\n/**\n * This exception is thrown when the challenge email message is invalid.\n * <p>\n * If this exception is thrown, the challenge message does not match the actual challenge\n * or has other issues. It <em>must</em> be rejected.\n * <p>\n * Reasons may be (for example):\n * <ul>\n *     <li>Unexpected sender address</li>\n *     <li>Bad S/MIME signature</li>\n * </ul>\n *\n * @since 2.15\n */\npublic class AcmeInvalidMessageException extends AcmeException {\n    @Serial\n    private static final long serialVersionUID = 5607857024718309330L;\n\n    private final List<ErrorBundle> errors;\n\n    /**\n     * Creates a new {@link AcmeInvalidMessageException}.\n     *\n     * @param msg\n     *         Reason of the exception\n     */\n    public AcmeInvalidMessageException(String msg) {\n        super(msg);\n        this.errors = Collections.emptyList();\n    }\n\n    /**\n     * Creates a new {@link AcmeInvalidMessageException}.\n     *\n     * @param msg\n     *         Reason of the exception\n     * @param errors\n     *         List of {@link ErrorBundle} with further details\n     * @since 2.16\n     */\n    public AcmeInvalidMessageException(String msg, List<ErrorBundle> errors) {\n        super(msg);\n        this.errors = unmodifiableList(errors);\n    }\n\n    /**\n     * Creates a new {@link AcmeInvalidMessageException}.\n     *\n     * @param msg\n     *         Reason of the exception\n     * @param cause\n     *         Cause\n     */\n    public AcmeInvalidMessageException(String msg, Throwable cause) {\n        super(msg, cause);\n        var errors = new ArrayList<ErrorBundle>(1);\n        Optional.ofNullable(cause)\n                .filter(LocalizedException.class::isInstance)\n                .map(LocalizedException.class::cast)\n                .map(LocalizedException::getErrorMessage)\n                .ifPresent(errors::add);\n        this.errors = unmodifiableList(errors);\n    }\n\n    /**\n     * Returns a list with further error details, if available. The list may be empty, but\n     * is never {@code null}.\n     *\n     * @since 2.16\n     */\n    public List<ErrorBundle> getErrors() {\n        return errors;\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/exception/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2022 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * Exceptions that are related to S/MIME signatures.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.smime.exception;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * An acme4j extension that provides S/MIME certificate features.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.smime;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/wrapper/Mail.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2023 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.wrapper;\n\nimport java.util.Collection;\nimport java.util.Optional;\n\nimport jakarta.mail.internet.InternetAddress;\nimport org.shredzone.acme4j.smime.exception.AcmeInvalidMessageException;\n\n/**\n * Provide access to all required fields of an email. The underlying implementation\n * has to take care about parsing, validation, and verification of security features.\n *\n * @since 2.16\n */\npublic interface Mail {\n\n    /**\n     * Returns the sender address.\n     *\n     * @throws AcmeInvalidMessageException\n     *         if there is not exactly one \"From\" header, or if the sender address is\n     *         invalid.\n     */\n    InternetAddress getFrom() throws AcmeInvalidMessageException;\n\n    /**\n     * Returns the recipient address.\n     *\n     * @throws AcmeInvalidMessageException\n     *         if there is not exactly one \"To\" header, or if the recipient address is\n     *         invalid.\n     */\n    InternetAddress getTo() throws AcmeInvalidMessageException;\n\n    /**\n     * Returns the subject.\n     *\n     * @throws AcmeInvalidMessageException if there is no \"Subject\" header.\n     */\n    String getSubject() throws AcmeInvalidMessageException;\n\n    /**\n     * Returns a collection of the reply-to addresses. Reply-to addresses that are not\n     * {@link InternetAddress} type are ignored.\n     *\n     * @return Collection of reply-to addresses. May be empty, but is never {@code null}.\n     * @throws AcmeInvalidMessageException\n     *         if the \"Reply-To\" header could not be parsed\n     */\n    Collection<InternetAddress> getReplyTo() throws AcmeInvalidMessageException;\n\n    /**\n     * Returns the message ID.\n     *\n     * @return Message ID, or empty if there is no message ID header.\n     * @throws AcmeInvalidMessageException\n     *         if the \"Message-ID\" header could not be parsed\n     */\n    Optional<String> getMessageId() throws AcmeInvalidMessageException;\n\n    /**\n     * Checks if the mail was flagged as auto-generated.\n     *\n     * @return {@code true} if there is an \"Auto-Submitted\" header containing the string\n     * \"auto-generated\", {@code false} otherwise.\n     * @throws AcmeInvalidMessageException\n     *         if the \"Auto-Submitted\" header could not be parsed.\n     */\n    boolean isAutoSubmitted() throws AcmeInvalidMessageException;\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/wrapper/SignedMail.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2023 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.wrapper;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Enumeration;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.TreeSet;\nimport java.util.stream.Collectors;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport jakarta.mail.Header;\nimport jakarta.mail.Message;\nimport jakarta.mail.internet.AddressException;\nimport jakarta.mail.internet.InternetAddress;\nimport org.bouncycastle.asn1.ASN1Enumerated;\nimport org.bouncycastle.asn1.ASN1Integer;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.ASN1Sequence;\nimport org.bouncycastle.asn1.ASN1Set;\nimport org.bouncycastle.asn1.ASN1String;\nimport org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;\nimport org.bouncycastle.cms.SignerInformation;\nimport org.shredzone.acme4j.smime.exception.AcmeInvalidMessageException;\n\n/**\n * Represents a signed {@link Message}.\n * <p>\n * This class is generated by {@link SignedMailBuilder}, which also takes care for\n * signature verification and validation.\n *\n * @see <a href=\"https://www.rfc-editor.org/rfc/rfc7508.html\">RFC 7508</a>\n * @since 2.16\n */\npublic class SignedMail implements Mail {\n    private static final ASN1ObjectIdentifier SECURE_HEADER_FIELDS_ID\n            = PKCSObjectIdentifiers.pkcs_9.branch(\"16.2.55\");\n    private static final Set<String> IGNORE_HEADERS\n            = Set.of(\"CONTENT-TYPE\", \"MIME-VERSION\", \"RECEIVED\");\n    private static final Set<String> REQUIRED_HEADERS\n            = Set.of(\"FROM\", \"TO\", \"SUBJECT\");\n\n    private final List<MailHeader> headers = new ArrayList<>();\n\n    /**\n     * This class is to be constructed only by {@link SignedMailBuilder}.\n     */\n    SignedMail() {\n        // package protected constructor\n    }\n\n    /**\n     * Imports untrusted headers from the envelope message.\n     * <p>\n     * All previously imported headers are cleaned before that.\n     */\n    public void importUntrustedHeaders(Enumeration<Header> en) {\n        headers.clear();\n        while (en.hasMoreElements()) {\n            var h = en.nextElement();\n            var name = h.getName();\n            if (IGNORE_HEADERS.contains(name.toUpperCase(Locale.ENGLISH))) {\n                continue;\n            }\n\n            headers.add(new MailHeader(name, h.getValue()));\n        }\n    }\n\n    /**\n     * Imports secured headers from the signed, inner message.\n     * <p>\n     * The import is strict. If a secured header is also present in the envelope message,\n     * it must match exactly.\n     *\n     * @throws AcmeInvalidMessageException\n     *         if the secured header was found in the envelope message, but did not match.\n     */\n    public void importTrustedHeaders(Enumeration<Header> en) throws AcmeInvalidMessageException {\n        while (en.hasMoreElements()) {\n            var h = en.nextElement();\n            var name = h.getName();\n            if (IGNORE_HEADERS.contains(name.toUpperCase(Locale.ENGLISH))) {\n                continue;\n            }\n\n            var value = h.getValue();\n            var count = headers.stream()\n                    .filter(mh -> mh.nameEquals(name, false) && mh.valueEquals(value, false))\n                    .peek(MailHeader::setTrusted)\n                    .count();\n\n            if (count == 0) {\n                throw new AcmeInvalidMessageException(\"Secured header '\" + name\n                        + \"' does not match envelope header\");\n            }\n        }\n    }\n\n    /**\n     * Imports secured headers from the signed, inner message.\n     * <p>\n     * The import is relaxed. If the secured header is also found in the envelope message\n     * header, it will replace the envelope header.\n     */\n    public void importTrustedHeadersRelaxed(Enumeration<Header> en) {\n        while (en.hasMoreElements()) {\n            var h = en.nextElement();\n            var name = h.getName();\n            if (IGNORE_HEADERS.contains(name.toUpperCase(Locale.ENGLISH))) {\n                continue;\n            }\n\n            headers.removeIf(mh -> mh.nameEquals(name, true) && !mh.trusted);\n            headers.add(new MailHeader(name, h.getValue()).setTrusted());\n        }\n    }\n\n    /**\n     * Imports secured headers from the signature.\n     * <p>\n     * Depending on the signature, the envelope header is either checked, deleted, or\n     * modified.\n     *\n     * @throws AcmeInvalidMessageException\n     *         if the signature header conflicts with the envelope header.\n     */\n    public void importSignatureHeaders(SignerInformation si) throws AcmeInvalidMessageException {\n        var attr = si.getSignedAttributes().get(SECURE_HEADER_FIELDS_ID);\n        if (attr == null) {\n            return;\n        }\n\n        var relaxed = false;\n        for (var element : (ASN1Set) attr.getAttributeValues()[0]) {\n            if (element instanceof ASN1Enumerated asn1element) {\n                var algorithm = asn1element.intValueExact();\n                relaxed = switch (algorithm) {\n                    case 0 -> false;\n                    case 1 -> true;\n                    default -> throw new AcmeInvalidMessageException(\"Unknown algorithm: \" + algorithm);\n                };\n            }\n        }\n\n        for (var element : (ASN1Set) attr.getAttributeValues()[0]) {\n            if (element instanceof ASN1Sequence asn1sequence) {\n                for (var sequenceElement : asn1sequence) {\n                    var headerField = (ASN1Sequence) sequenceElement;\n                    var fieldName = ((ASN1String) headerField.getObjectAt(0)).getString();\n                    var fieldValue = ((ASN1String) headerField.getObjectAt(1)).getString();\n                    var fieldStatus = 0;\n                    if (headerField.size() >= 3) {\n                        fieldStatus = ((ASN1Integer) headerField.getObjectAt(2)).intValueExact();\n                    }\n                    switch (fieldStatus) {\n                        case 0:\n                            checkDuplicatedField(fieldName, fieldValue, relaxed);\n                            break;\n                        case 1:\n                            deleteField(fieldName, fieldValue, relaxed);\n                            break;\n                        case 2:\n                            modifyField(fieldName, fieldValue, relaxed);\n                            break;\n                        default:\n                            throw new AcmeInvalidMessageException(\"Unknown field status \" + fieldStatus);\n                    }\n                }\n            }\n        }\n    }\n\n    @Override\n    public InternetAddress getFrom() throws AcmeInvalidMessageException {\n        try {\n            return new InternetAddress(fetchTrustedHeader(\"FROM\"));\n        } catch (AddressException ex) {\n            throw new AcmeInvalidMessageException(\"Invalid 'FROM' address\", ex);\n        }\n    }\n\n    @Override\n    public InternetAddress getTo() throws AcmeInvalidMessageException {\n        try {\n            return new InternetAddress(fetchTrustedHeader(\"TO\"));\n        } catch (AddressException ex) {\n            throw new AcmeInvalidMessageException(\"Invalid 'TO' address\", ex);\n        }\n    }\n\n    @Override\n    public String getSubject() throws AcmeInvalidMessageException {\n        return fetchTrustedHeader(\"SUBJECT\");\n    }\n\n    @Override\n    public Optional<String> getMessageId() {\n        return headers.stream()\n                .filter(mh -> \"MESSAGE-ID\".equalsIgnoreCase(mh.name))\n                .map(mh -> mh.value)\n                .map(String::trim)\n                .findFirst();\n    }\n\n    @Override\n    public Collection<InternetAddress> getReplyTo() throws AcmeInvalidMessageException {\n        var replyToList = headers.stream()\n                .filter(mh -> \"REPLY-TO\".equalsIgnoreCase(mh.name))\n                .map(mh -> mh.value)\n                .map(String::trim)\n                .collect(Collectors.toList());\n\n        if (replyToList.isEmpty()) {\n            return Collections.emptyList();\n        }\n\n        try {\n            var result = new ArrayList<InternetAddress>(replyToList.size());\n            for (var replyTo : replyToList) {\n                result.add(new InternetAddress(replyTo));\n            }\n            return Collections.unmodifiableList(result);\n        } catch (AddressException ex) {\n            throw new AcmeInvalidMessageException(\"Invalid 'REPLY-TO' address\", ex);\n        }\n    }\n\n    @Override\n    public boolean isAutoSubmitted() {\n        return headers.stream()\n                .filter(mh -> \"AUTO-SUBMITTED\".equalsIgnoreCase(mh.name))\n                .map(mh -> mh.value)\n                .map(String::trim)\n                .map(mh -> mh.toLowerCase(Locale.ENGLISH))\n                .anyMatch(h -> h.equals(\"auto-generated\") || h.startsWith(\"auto-generated;\"));\n    }\n\n    /**\n     * Returns a set of missing, but required secured headers. This list is supposed to\n     * be empty on valid messages with secured headers. If there is at least one element,\n     * the message must be refused.\n     */\n    public Set<String> getMissingSecuredHeaders() {\n        var missing = new TreeSet<>(REQUIRED_HEADERS);\n        headers.stream()\n                .filter(mh -> mh.trusted)\n                .map(mh -> mh.name)\n                .map(mh -> mh.toUpperCase(Locale.ENGLISH))\n                .forEach(missing::remove);\n        return missing;\n    }\n\n    /**\n     * Processes a \"duplicated\" header field status. The signature header must be found\n     * with the same value in the envelope message header.\n     *\n     * @param header\n     *         Header name\n     * @param value\n     *         Expected header value\n     * @param relaxed\n     *         {@code false}: simple, {@code true}: relaxed algorithm\n     * @throws AcmeInvalidMessageException\n     *         if a header with the same value was not found\n     */\n    protected void checkDuplicatedField(String header, String value, boolean relaxed) throws AcmeInvalidMessageException {\n        var count = headers.stream()\n                .filter(mh -> mh.nameEquals(header, relaxed) && mh.valueEquals(value, relaxed))\n                .peek(MailHeader::setTrusted)\n                .count();\n        if (count == 0) {\n            throw new AcmeInvalidMessageException(\"Secured header '\" + header\n                    + \"' was not found in envelope header\");\n        }\n    }\n\n    /**\n     * Processes a \"deleted\" header field status. The signature header must be found\n     * with the same value in the envelope message header, and is then removed from the\n     * header.\n     *\n     * @param header\n     *         Header name\n     * @param value\n     *         Expected header value\n     * @param relaxed\n     *         {@code false}: simple, {@code true}: relaxed algorithm\n     * @throws AcmeInvalidMessageException\n     *         if a header with the same value was not found\n     */\n    protected void deleteField(String header, String value, boolean relaxed) throws AcmeInvalidMessageException {\n        if (!headers.removeIf(mh -> mh.nameEquals(header, relaxed) && mh.valueEquals(value, relaxed))) {\n            throw new AcmeInvalidMessageException(\"Secured header '\" + header\n                    + \"' was not found in envelope header for deletion\");\n        }\n    }\n\n    /**\n     * Processes a \"modified\" header field status. The signature header must be found in\n     * the envelope message header, and is then replaced with the given value.\n     *\n     * @param header\n     *         Header name\n     * @param value\n     *         New header value\n     * @param relaxed\n     *         {@code false}: simple, {@code true}: relaxed algorithm\n     * @throws AcmeInvalidMessageException\n     *         if the header was not found\n     */\n    protected void modifyField(String header, String value, boolean relaxed) throws AcmeInvalidMessageException {\n        if (!headers.removeIf(mh -> mh.nameEquals(header, relaxed))) {\n            throw new AcmeInvalidMessageException(\"Secured header '\" + header\n                    + \"' was not found in envelope header for modification\");\n        }\n        headers.add(new MailHeader(header, value).setTrusted());\n    }\n\n    /**\n     * Fetches a trusted header. The header must be present exactly once, and must be\n     * marked as trusted, i.e. it was either found in the signed inner message, or was\n     * set by the signature headers.\n     *\n     * @param name\n     *         Name of the header, case-insensitive\n     * @return Header value\n     * @throws AcmeInvalidMessageException\n     *         if the header was not found, was found more than once, or is not marked as\n     *         trusted\n     */\n    private String fetchTrustedHeader(String name) throws AcmeInvalidMessageException {\n        var candidates = headers.stream()\n                .filter(mh -> name.equalsIgnoreCase(mh.name))\n                .filter(mh -> mh.trusted)\n                .map(mh -> mh.value)\n                .map(String::trim)\n                .collect(Collectors.toList());\n\n        if (candidates.isEmpty()) {\n            throw new AcmeInvalidMessageException(\"Protected '\" + name\n                    + \"' header is required, but missing\");\n        }\n\n        if (candidates.size() > 1) {\n            throw new AcmeInvalidMessageException(\"Expecting exactly one protected '\"\n                    + name + \"' header, but found \" + candidates.size());\n        }\n\n        return candidates.get(0);\n    }\n\n    @Override\n    public String toString() {\n        var sb = new StringBuilder();\n        for (var mh : headers) {\n            sb.append(mh.toString()).append('\\n');\n        }\n        return sb.toString();\n    }\n\n    /**\n     * A single mail header.\n     */\n    private static class MailHeader {\n        public final String name;\n        public final String value;\n        public boolean trusted;\n\n        /**\n         * Creates a new mail header.\n         *\n         * @param name Header name\n         * @param value Header value\n         */\n        public MailHeader(String name, String value) {\n            this.name = name;\n            this.value = value;\n        }\n\n        /**\n         * Marks this header as trusted.\n         *\n         * @return itself\n         */\n        public MailHeader setTrusted() {\n            trusted = true;\n            return this;\n        }\n\n        /**\n         * Checks if the header name equals the expected value.\n         *\n         * @param expected\n         *         Expected name\n         * @param relaxed\n         *         {@code false}: names must match exactly, {@code true}: case-insensitive\n         *         match\n         * @return {@code true} if equal\n         */\n        public boolean nameEquals(@Nullable String expected, boolean relaxed) {\n            if (!relaxed) {\n                return name.equals(expected);\n            }\n\n            if (expected == null) {\n                return false;\n            }\n\n            return name.equalsIgnoreCase(expected);\n        }\n\n        /**\n         * Checks if the header value equals the expected value.\n         *\n         * @param expected\n         *         Expected value, may be {@code null}\n         * @param relaxed\n         *         {@code false}: value must match exactly, {@code true}: differences in\n         *         whitespaces are ignored\n         * @return {@code true} if equal\n         */\n        public boolean valueEquals(@Nullable String expected, boolean relaxed) {\n            if (!relaxed) {\n                return value.equals(expected);\n            }\n\n            if (expected == null) {\n                return false;\n            }\n\n            var normalizedValue = value.replaceAll(\"\\\\s+\", \" \").trim();\n            var normalizedExpected = expected.replaceAll(\"\\\\s+\", \" \").trim();\n            return normalizedValue.equals(normalizedExpected);\n        }\n\n        @Override\n        public String toString() {\n            return (trusted ? \"* \" : \"  \") + name + \": \" + value;\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/wrapper/SignedMailBuilder.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2023 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.wrapper;\n\nimport static java.util.Objects.requireNonNull;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.security.InvalidAlgorithmParameterException;\nimport java.security.KeyStore;\nimport java.security.KeyStoreException;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.cert.CertificateException;\nimport java.security.cert.PKIXParameters;\nimport java.security.cert.X509Certificate;\nimport java.util.Collection;\nimport java.util.Properties;\nimport java.util.Set;\nimport java.util.concurrent.atomic.AtomicReference;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport jakarta.mail.Message;\nimport jakarta.mail.MessagingException;\nimport jakarta.mail.Session;\nimport jakarta.mail.internet.AddressException;\nimport jakarta.mail.internet.InternetAddress;\nimport jakarta.mail.internet.MimeMessage;\nimport jakarta.mail.internet.MimeMultipart;\nimport org.bouncycastle.asn1.x509.Extension;\nimport org.bouncycastle.asn1.x509.GeneralName;\nimport org.bouncycastle.asn1.x509.GeneralNames;\nimport org.bouncycastle.cert.X509CertificateHolder;\nimport org.bouncycastle.cms.CMSException;\nimport org.bouncycastle.cms.SignerInformation;\nimport org.bouncycastle.mail.smime.SMIMESigned;\nimport org.bouncycastle.mail.smime.validator.SignedMailValidator;\nimport org.bouncycastle.mail.smime.validator.SignedMailValidatorException;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.smime.exception.AcmeInvalidMessageException;\n\n/**\n * Creates a {@link SignedMail} instance from a signed message.\n *\n * @since 2.16\n */\npublic class SignedMailBuilder {\n    private static final AtomicReference<KeyStore> CACERTS_TRUSTSTORE = new AtomicReference<>();\n\n    private Session mailSession = Session.getDefaultInstance(new Properties());\n    private volatile boolean relaxed = false;\n\n    @Nullable\n    private PKIXParameters pkixParameters = null;\n\n    /**\n     * Uses the standard cacerts truststore. This is the default.\n     */\n    public SignedMailBuilder withCaCertsTrustStore() {\n        pkixParameters = null;\n        return this;\n    }\n\n    /**\n     * Uses the given {@link X509Certificate} for certificate validation.\n     * <p>\n     * This is for self-signed certificates. No revocation checks will take place.\n     *\n     * @param signCert {@link X509Certificate} to use.\n     * @return itself\n     */\n    public SignedMailBuilder withSignCert(X509Certificate signCert) {\n        requireNonNull(signCert, \"signCert\");\n        try {\n            KeyStore ks = KeyStore.getInstance(\"JKS\");\n            ks.load(null, null);\n            ks.setCertificateEntry(\"cert\", signCert);\n            return withTrustStore(ks);\n        } catch (KeyStoreException | IOException | NoSuchAlgorithmException |\n                 CertificateException | InvalidAlgorithmParameterException ex) {\n            throw new IllegalArgumentException(\"Invalid certificate\", ex);\n        }\n    }\n\n    /**\n     * Uses the given truststore for certificate validation.\n     * <p>\n     * This is for self-signed certificates. No revocation checks will take place.\n     *\n     * @param trustStore {@link KeyStore} to use.\n     * @return itself\n     */\n    public SignedMailBuilder withTrustStore(KeyStore trustStore)\n            throws KeyStoreException, InvalidAlgorithmParameterException {\n        requireNonNull(trustStore, \"trustStore\");\n        PKIXParameters param = new PKIXParameters(trustStore);\n        param.setRevocationEnabled(false);\n        return withPKIXParameters(param);\n    }\n\n    /**\n     * Uses the given {@link PKIXParameters} for certificate validation.\n     *\n     * @param param {@link PKIXParameters} to use.\n     * @return itself\n     */\n    public SignedMailBuilder withPKIXParameters(PKIXParameters param) {\n        this.pkixParameters = requireNonNull(param, \"param\");\n        return this;\n    }\n\n    /**\n     * Sets a different mail {@link Session} that is used for accessing the signed\n     * email body.\n     *\n     * @param mailSession {@link Session} to use.\n     * @return itself\n     */\n    public SignedMailBuilder withMailSession(Session mailSession) {\n        this.mailSession = requireNonNull(mailSession, \"mailSession\");\n        return this;\n    }\n\n    /**\n     * Changes relaxed validation. If enabled, headers of the signed message body are\n     * preferred if present, but do not need to match the appropriate headers of the\n     * envelope message.\n     * <p>\n     * By default, relaxed validation is disabled.\n     *\n     * @param relaxed sets relaxed validation mode\n     * @return itself\n     */\n    public SignedMailBuilder relaxed(boolean relaxed) {\n        this.relaxed = relaxed;\n        return this;\n    }\n\n    /**\n     * Validates the message signature and message headers. If validation passes, a\n     * {@link SignedMail} instance is returned that gives access to the trusted mail\n     * headers.\n     *\n     * @param message {@link Message}, must be a {@link MimeMessage}.\n     * @return SignedMail containing the trusted headers.\n     * @throws AcmeInvalidMessageException\n     *         if the given message is invalid, its signature is invalid, or the secured\n     *         headers are invalid. If this exception is thrown, the message MUST be\n     *         rejected.\n     */\n    public SignedMail build(Message message) throws AcmeInvalidMessageException {\n        requireNonNull(message, \"message\");\n        try {\n            // Check all parameters\n            if (!(message instanceof MimeMessage mimeMessage)) {\n                throw new IllegalArgumentException(\"Message must be a MimeMessage\");\n            }\n\n            if (!(mimeMessage.getContent() instanceof MimeMultipart contentMultipart)) {\n                throw new AcmeProtocolException(\"S/MIME signed message must contain MimeMultipart\");\n            }\n\n            if (pkixParameters == null) {\n                pkixParameters = new PKIXParameters(getCaCertsTrustStore());\n            }\n\n            // Get the signed message\n            SMIMESigned signed = new SMIMESigned(contentMultipart);\n\n            // Validate the signature\n            SignerInformation si = validateSignature(mimeMessage, pkixParameters);\n\n            // Collect the headers\n            SignedMail result = new SignedMail();\n\n            // First import all untrusted headers from the envelope message\n            result.importUntrustedHeaders(mimeMessage.getAllHeaders());\n\n            // If there is an inner, signed message, import all signed headers\n            MimeMessage content = signed.getContentAsMimeMessage(mailSession);\n            if (content != null && content.isMimeType(\"message/rfc822\")) {\n                MimeMessage protectedBody = new MimeMessage(mailSession, content.getInputStream());\n                if (relaxed) {\n                    result.importTrustedHeadersRelaxed(protectedBody.getAllHeaders());\n                } else {\n                    result.importTrustedHeaders(protectedBody.getAllHeaders());\n                }\n            }\n\n            // Import secured headers from the signature, if present\n            result.importSignatureHeaders(si);\n\n            // Check if all mandatory headers are trusted\n            Set<String> missing = result.getMissingSecuredHeaders();\n            if (!missing.isEmpty()) {\n                throw new AcmeInvalidMessageException(\"Secured headers expected, but missing: \"\n                        + String.join(\", \", missing));\n            }\n\n            // Check if the signer matches the mail sender\n            InternetAddress signerAddress = validateSigatureSender(signed, si);\n            if (!result.getFrom().equals(signerAddress)) {\n                throw new AcmeInvalidMessageException(\"Message is not signed by the expected sender\");\n            }\n\n            return result;\n        } catch (IOException | MessagingException | CMSException |\n                 KeyStoreException | InvalidAlgorithmParameterException ex) {\n            throw new AcmeInvalidMessageException(\"Could not validate message signature\", ex);\n        }\n    }\n\n    /**\n     * Validates the signature of the signed message.\n     *\n     * @return The {@link SignerInformation} of the valid signature.\n     * @throws AcmeInvalidMessageException\n     *         if the signature is invalid, or if the message was signed with more than\n     *         one signature.\n     */\n    @SuppressWarnings(\"unchecked\")\n    private SignerInformation validateSignature(MimeMessage message, PKIXParameters pkixParameters)\n            throws AcmeInvalidMessageException {\n        try {\n            var smv = new SignedMailValidator(message, pkixParameters);\n\n            var store = smv.getSignerInformationStore();\n            if (store.size() != 1) {\n                throw new AcmeInvalidMessageException(\"Expected exactly one signer, but found \" + store.size());\n            }\n\n            var si = store.getSigners().iterator().next();\n            var vr = smv.getValidationResult(si);\n            if (!vr.isValidSignature()) {\n                throw new AcmeInvalidMessageException(\"Invalid signature\", vr.getErrors());\n            }\n            return si;\n        } catch (SignedMailValidatorException ex) {\n            throw new AcmeInvalidMessageException(\"Cannot validate signature\", ex);\n        }\n    }\n\n    /**\n     * Validates the signature of the sender. It MUST contain a subjectAltName extension\n     * with a rfc822Name that matches the sender.\n     *\n     * @param signed\n     *         {@link SMIMESigned} of the signed message\n     * @param si\n     *         {@link SignerInformation} of the message signer\n     * @return The {@link InternetAddress} of the rfc822Name found in the subjectAltName\n     * @throws AcmeInvalidMessageException\n     *         if no signature was found, or if the signature has no subjectAltName\n     *         extension with rfc822Name.\n     */\n    @SuppressWarnings(\"unchecked\")\n    private InternetAddress validateSigatureSender(SMIMESigned signed, SignerInformation si)\n            throws AcmeInvalidMessageException {\n        Collection<X509CertificateHolder> certCollection = signed.getCertificates().getMatches(si.getSID());\n        if (certCollection.isEmpty()) {\n            throw new AcmeInvalidMessageException(\"Could not find certificate for signer ID \"\n                    + si.getSID().toString());\n        }\n        var ch = certCollection.iterator().next();\n\n        var gns = GeneralNames.fromExtensions(ch.getExtensions(), Extension.subjectAlternativeName);\n        if (gns == null) {\n            throw new AcmeInvalidMessageException(\"Certificate does not have a subjectAltName extension\");\n        }\n\n        for (var name : gns.getNames()) {\n            if (name.getTagNo() == GeneralName.rfc822Name) {\n                try {\n                    return new InternetAddress(name.getName().toString());\n                } catch (AddressException ex) {\n                    throw new AcmeInvalidMessageException(\"Invalid certificate email address: \"\n                            + name.getName().toString(), ex);\n                }\n            }\n        }\n\n        throw new AcmeInvalidMessageException(\"No rfc822Name found in subjectAltName extension\");\n    }\n\n    /**\n     * Generates a truststore from Java's own cacerts file. The result is cached.\n     *\n     * @return CaCerts truststore\n     */\n    protected static KeyStore getCaCertsTrustStore() {\n        var caCerts = CACERTS_TRUSTSTORE.get();\n        if (caCerts == null) {\n            var javaHome = System.getProperty(\"java.home\");\n            var caFileName = javaHome + File.separator + \"lib\" + File.separator\n                    + \"security\" + File.separator + \"cacerts\";\n\n            try (var in = new FileInputStream(caFileName)) {\n                caCerts = KeyStore.getInstance(\"JKS\");\n                caCerts.load(in, \"changeit\".toCharArray());\n                CACERTS_TRUSTSTORE.set(caCerts);\n            } catch (KeyStoreException | IOException | CertificateException |\n                     NoSuchAlgorithmException ex) {\n                throw new IllegalStateException(\"Cannot access cacerts\", ex);\n            }\n        }\n        return caCerts;\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/wrapper/SimpleMail.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2023 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.wrapper;\n\nimport static java.util.Objects.requireNonNull;\nimport static java.util.stream.Collectors.toUnmodifiableList;\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Locale;\nimport java.util.Optional;\n\nimport jakarta.mail.Message;\nimport jakarta.mail.MessagingException;\nimport jakarta.mail.internet.InternetAddress;\nimport org.shredzone.acme4j.smime.exception.AcmeInvalidMessageException;\n\n/**\n * Represents a simple, unsigned {@link Message}.\n * <p>\n * There is no signature validation at all. Use this class only for testing purposes,\n * or if a validation has already been performed in a separate step.\n *\n * @since 2.16\n */\npublic class SimpleMail implements Mail {\n    private static final String HEADER_MESSAGE_ID = \"Message-ID\";\n    private static final String HEADER_AUTO_SUBMITTED = \"Auto-Submitted\";\n\n    private final Message message;\n\n    public SimpleMail(Message message) {\n        this.message = requireNonNull(message, \"message\");\n    }\n\n    @Override\n    public InternetAddress getFrom() throws AcmeInvalidMessageException {\n        try {\n            var from = message.getFrom();\n            if (from == null) {\n                throw new AcmeInvalidMessageException(\"Missing required 'From' header\");\n            }\n            if (from.length != 1) {\n                throw new AcmeInvalidMessageException(\"Message must have exactly one sender, but has \" + from.length);\n            }\n            if (!(from[0] instanceof InternetAddress from0)) {\n                throw new AcmeInvalidMessageException(\"Invalid sender message type: \" + from[0].getClass().getName());\n            }\n            return from0;\n        } catch (MessagingException ex) {\n            throw new AcmeInvalidMessageException(\"Could not read 'From' header\", ex);\n        }\n    }\n\n    @Override\n    public InternetAddress getTo() throws AcmeInvalidMessageException {\n        try {\n            var to = message.getRecipients(Message.RecipientType.TO);\n            if (to == null) {\n                throw new AcmeInvalidMessageException(\"Missing required 'To' header\");\n            }\n            if (to.length != 1) {\n                throw new AcmeInvalidMessageException(\"Message must have exactly one recipient, but has \" + to.length);\n            }\n            if (!(to[0] instanceof InternetAddress to0)) {\n                throw new AcmeInvalidMessageException(\"Invalid recipient message type: \" + to[0].getClass().getName());\n            }\n            return to0;\n        } catch (MessagingException ex) {\n            throw new AcmeInvalidMessageException(\"Could not read 'To' header\", ex);\n        }\n    }\n\n    @Override\n    public String getSubject() throws AcmeInvalidMessageException {\n        try {\n            var subject = message.getSubject();\n            if (subject == null) {\n                throw new AcmeInvalidMessageException(\"Message must have a subject\");\n            }\n            return subject;\n        } catch (MessagingException ex) {\n            throw new AcmeInvalidMessageException(\"Could not read 'Subject' header\", ex);\n        }\n    }\n\n    @Override\n    public Collection<InternetAddress> getReplyTo() throws AcmeInvalidMessageException {\n        try {\n            var rto = message.getReplyTo();\n            if (rto == null) {\n                return Collections.emptyList();\n            }\n            return Arrays.stream(rto)\n                    .filter(InternetAddress.class::isInstance)\n                    .map(InternetAddress.class::cast)\n                    .collect(toUnmodifiableList());\n        } catch (MessagingException ex) {\n            throw new AcmeInvalidMessageException(\"Could not read 'Reply-To' header\", ex);\n        }\n    }\n\n    @Override\n    public Optional<String> getMessageId() throws AcmeInvalidMessageException {\n        try {\n            var mid = message.getHeader(HEADER_MESSAGE_ID);\n            if (mid == null || mid.length == 0) {\n                return Optional.empty();\n            }\n            if (mid.length > 1) {\n                throw new AcmeInvalidMessageException(\"Expected one Message-ID, but found \" + mid.length);\n            }\n            return Optional.of(mid[0]);\n        } catch (MessagingException ex) {\n            throw new AcmeInvalidMessageException(\"Could not read '\" + HEADER_MESSAGE_ID + \"' header\", ex);\n        }\n    }\n\n    @Override\n    public boolean isAutoSubmitted() throws AcmeInvalidMessageException {\n        try {\n            var autoSubmitted = message.getHeader(HEADER_AUTO_SUBMITTED);\n            if (autoSubmitted == null) {\n                return false;\n            }\n            return Arrays.stream(autoSubmitted)\n                    .map(String::trim)\n                    .map(as -> as.toLowerCase(Locale.ENGLISH))\n                    .anyMatch(h -> h.equals(\"auto-generated\") || h.startsWith(\"auto-generated;\"));\n        } catch (MessagingException ex) {\n            throw new AcmeInvalidMessageException(\"Could not read '\" + HEADER_AUTO_SUBMITTED + \"' header\", ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/main/java/org/shredzone/acme4j/smime/wrapper/package-info.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2023 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * {@link org.shredzone.acme4j.smime.wrapper.Mail} is a wrapper interface which provides\n * access to all relevant headers of the validation email. Usually\n * {@link org.shredzone.acme4j.smime.wrapper.SignedMailBuilder} is used for parsing the\n * email and validating the signature.\n * {@link org.shredzone.acme4j.smime.wrapper.SimpleMail} is a simple implementation that\n * should only be used for testing purposes or after an external validation.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.acme4j.smime.wrapper;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "acme4j-smime/src/main/resources/.gitignore",
    "content": ""
  },
  {
    "path": "acme4j-smime/src/main/resources/META-INF/services/org.shredzone.acme4j.provider.ChallengeProvider",
    "content": "\n# Challenge Provider for https://datatracker.ietf.org/doc/html/rfc8823\norg.shredzone.acme4j.smime.challenge.EmailReply00ChallengeProvider"
  },
  {
    "path": "acme4j-smime/src/test/java/.gitignore",
    "content": ""
  },
  {
    "path": "acme4j-smime/src/test/java/org/shredzone/acme4j/smime/EmailIdentifierTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.stream.Stream;\n\nimport jakarta.mail.internet.AddressException;\nimport jakarta.mail.internet.InternetAddress;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\n/**\n * Tests of {@link EmailIdentifier}.\n */\npublic class EmailIdentifierTest {\n\n    @Test\n    public void testConstants() {\n        assertThat(EmailIdentifier.TYPE_EMAIL).isEqualTo(\"email\");\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"provideTestEmails\")\n    public void testEmail(Object input, String expected) {\n        var id = input instanceof InternetAddress internetAddress\n                ? EmailIdentifier.email(internetAddress)\n                : EmailIdentifier.email(input.toString());\n\n        assertThat(id.getType()).isEqualTo(EmailIdentifier.TYPE_EMAIL);\n        assertThat(id.getValue()).isEqualTo(expected);\n        assertThat(id.getEmailAddress().getAddress()).isEqualTo(expected);\n    }\n\n    public static Stream<Arguments> provideTestEmails() throws AddressException {\n        return Stream.of(\n                Arguments.of(\"email@example.com\", \"email@example.com\"),\n                Arguments.of(new InternetAddress(\"email@example.com\"), \"email@example.com\"),\n                Arguments.of(new InternetAddress(\"Example Corp <info@example.com>\"), \"info@example.com\")\n        );\n    }\n\n}"
  },
  {
    "path": "acme4j-smime/src/test/java/org/shredzone/acme4j/smime/SMIMETests.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.io.UncheckedIOException;\nimport java.nio.charset.StandardCharsets;\nimport java.security.KeyPair;\nimport java.security.cert.CertificateException;\nimport java.security.cert.CertificateFactory;\nimport java.security.cert.X509Certificate;\nimport java.util.Properties;\n\nimport jakarta.mail.Message;\nimport jakarta.mail.MessagingException;\nimport jakarta.mail.Session;\nimport jakarta.mail.internet.InternetAddress;\nimport jakarta.mail.internet.MimeMessage;\nimport org.shredzone.acme4j.Login;\nimport org.shredzone.acme4j.smime.challenge.EmailReply00Challenge;\nimport org.shredzone.acme4j.toolbox.JSON;\nimport org.shredzone.acme4j.util.KeyPairUtils;\n\n/**\n * Some common helper methods for S/MIME unit tests.\n */\npublic abstract class SMIMETests {\n    public static final String TOKEN_PART1 = \"LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\";\n    public static final String TOKEN_PART2 = \"DGyRejmCefe7v4NfDGDKfA\";\n    public static final String TOKEN = TOKEN_PART1 + TOKEN_PART2;\n    public static final String KEY_AUTHORIZATION = \"AjXW0h9_4YMP6Sv-9tKQNUrapI0us7ayBn0nCGOkUsk\";\n    public static final String RESPONSE_BODY = \"-----BEGIN ACME RESPONSE-----\\r\\n\"\n            + KEY_AUTHORIZATION + \"\\r\\n\"\n            + \"-----END ACME RESPONSE-----\\r\\n\";\n\n    protected final Session mailSession = Session.getDefaultInstance(new Properties());\n\n    /**\n     * Safely generates an {@link InternetAddress} from the given email address.\n     */\n    protected InternetAddress email(String address) {\n        try {\n            return new InternetAddress(address);\n        } catch (MessagingException ex) {\n            throw new IllegalArgumentException(ex);\n        }\n    }\n\n    /**\n     * Creates a mock {@link Message}.\n     *\n     * @param name\n     *         Name of the mock message to be read from the test resources.\n     * @return Mock {@link MimeMessage} that was created\n     */\n    protected MimeMessage mockMessage(String name) {\n        try (var in = SMIMETests.class.getResourceAsStream(\"/email/\" + name + \".eml\")) {\n            return new MimeMessage(mailSession, in);\n        } catch (IOException ex) {\n            throw new UncheckedIOException(ex);\n        } catch (MessagingException ex) {\n            throw new IllegalStateException(ex);\n        }\n    }\n\n    /**\n     * Returns a mock account key pair to be used for signing.\n     */\n    protected KeyPair mockAccountKey() {\n        try (var r = new InputStreamReader(\n                        SMIMETests.class.getResourceAsStream(\"/key.pem\"),\n                        StandardCharsets.UTF_8)) {\n            return KeyPairUtils.readKeyPair(r);\n        } catch (IOException ex) {\n            throw new UncheckedIOException(ex);\n        }\n    }\n\n    /**\n     * Returns a mock {@link Login} that can be used for signing.\n     */\n    protected Login mockLogin() {\n        var login = mock(Login.class);\n        when(login.getPublicKey()).thenReturn(mockAccountKey().getPublic());\n        return login;\n    }\n\n    /**\n     * Returns a mock {@link EmailReply00Challenge}.\n     *\n     * @param name\n     *         Resource name of the mock challenge\n     * @return Generated {@link EmailReply00Challenge}\n     */\n    protected EmailReply00Challenge mockChallenge(String name) {\n        return new EmailReply00Challenge(mockLogin(), getJSON(name));\n    }\n\n    /**\n     * Reads a JSON string from json test files and parses it.\n     *\n     * @param key\n     *            JSON resource\n     * @return Parsed JSON resource\n     */\n    protected JSON getJSON(String key) {\n        try {\n            return JSON.parse(SMIMETests.class.getResourceAsStream(\"/json/\" + key + \".json\"));\n        } catch (IOException ex) {\n            throw new UncheckedIOException(ex);\n        }\n    }\n\n    /**\n     * Reads a certificate from the given resource.\n     *\n     * @param name\n     *         Resource name of the certificate\n     * @return X509Certificate that was read\n     */\n    protected X509Certificate readCertificate(String name) throws IOException {\n        try (var in = SMIMETests.class.getResourceAsStream(\"/\" + name + \".pem\")) {\n            var cf = CertificateFactory.getInstance(\"X.509\");\n            return (X509Certificate) cf.generateCertificate(in);\n        } catch (CertificateException ex) {\n            throw new IOException(ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/test/java/org/shredzone/acme4j/smime/challenge/EmailReply00ChallengeTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.challenge;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.net.URI;\nimport java.net.URL;\n\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Status;\nimport org.shredzone.acme4j.provider.AbstractAcmeProvider;\nimport org.shredzone.acme4j.smime.SMIMETests;\n\n/**\n * Unit tests for {@link EmailReply00Challenge}.\n */\npublic class EmailReply00ChallengeTest extends SMIMETests {\n\n    /**\n     * Test that the challenge provider is found and the challenge is generated properly.\n     */\n    @Test\n    public void testCreateChallenge() {\n        var provider = new TestAcmeProvider();\n\n        var challenge = provider.createChallenge(mockLogin(), getJSON(\"emailReplyChallenge\"));\n        assertThat(challenge).isNotNull();\n        assertThat(challenge).isInstanceOf(EmailReply00Challenge.class);\n    }\n\n    /**\n     * Test that {@link EmailReply00Challenge} generates a correct authorization key.\n     */\n    @Test\n    public void testEmailReplyChallenge() {\n        var challenge = new EmailReply00Challenge(mockLogin(), getJSON(\"emailReplyChallenge\"));\n\n        assertThat(challenge.getType()).isEqualTo(EmailReply00Challenge.TYPE);\n        assertThat(challenge.getStatus()).isEqualTo(Status.PENDING);\n        assertThat(challenge.getToken(TOKEN_PART1)).isEqualTo(TOKEN_PART1 + TOKEN_PART2);\n        assertThat(challenge.getTokenPart2()).isEqualTo(TOKEN_PART2);\n        assertThat(challenge.getAuthorization(TOKEN_PART1)).isEqualTo(KEY_AUTHORIZATION);\n\n        assertThat(challenge.getFrom()).isEqualTo(\"acme-generator@example.org\");\n        assertThat(challenge.getExpectedSender().getAddress()).isEqualTo(\"acme-generator@example.org\");\n    }\n\n    /**\n     * Test that {@link EmailReply00Challenge#getAuthorization()} is not implemented.\n     */\n    @Test\n    public void testInvalidGetAuthorization() {\n        assertThrows(UnsupportedOperationException.class, () -> {\n            var challenge = new EmailReply00Challenge(mockLogin(), getJSON(\"emailReplyChallenge\"));\n            challenge.getAuthorization();\n        });\n    }\n\n    /**\n     * A minimal {@link AbstractAcmeProvider} implementation for testing the challenge\n     * builder.\n     */\n    private static class TestAcmeProvider extends AbstractAcmeProvider {\n        @Override\n        public boolean accepts(URI serverUri) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public URL resolve(URI serverUri) {\n            throw new UnsupportedOperationException();\n        }\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/test/java/org/shredzone/acme4j/smime/csr/SMIMECSRBuilderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.csr;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.*;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.StringReader;\nimport java.io.StringWriter;\nimport java.security.KeyPair;\nimport java.security.Security;\nimport java.util.Arrays;\n\nimport jakarta.mail.internet.AddressException;\nimport jakarta.mail.internet.InternetAddress;\nimport org.assertj.core.api.AutoCloseableSoftAssertions;\nimport org.bouncycastle.asn1.ASN1IA5String;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.DERBitString;\nimport org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;\nimport org.bouncycastle.asn1.x500.X500Name;\nimport org.bouncycastle.asn1.x500.style.BCStyle;\nimport org.bouncycastle.asn1.x509.Extension;\nimport org.bouncycastle.asn1.x509.Extensions;\nimport org.bouncycastle.asn1.x509.GeneralName;\nimport org.bouncycastle.asn1.x509.GeneralNames;\nimport org.bouncycastle.asn1.x509.KeyUsage;\nimport org.bouncycastle.jce.provider.BouncyCastleProvider;\nimport org.bouncycastle.openssl.PEMParser;\nimport org.bouncycastle.pkcs.PKCS10CertificationRequest;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.util.KeyPairUtils;\n\n/**\n * Unit tests for {@link SMIMECSRBuilder}.\n */\npublic class SMIMECSRBuilderTest {\n\n    private static KeyPair testKey;\n\n    @BeforeAll\n    public static void setup() {\n        Security.addProvider(new BouncyCastleProvider());\n\n        testKey = KeyPairUtils.createKeyPair(512);\n    }\n\n    /**\n     * Test if the generated S/MIME CSR is plausible.\n     */\n    @Test\n    public void testSMIMEGenerate() throws IOException, AddressException {\n        var builder = new SMIMECSRBuilder();\n        builder.addEmail(new InternetAddress(\"Contact <mail@example.com>\"));\n        builder.addEmail(new InternetAddress(\"Info <info@example.com>\"));\n        builder.addEmails(new InternetAddress(\"Sales Dept <sales@example.com>\"),\n                new InternetAddress(\"shop@example.com\"));\n        builder.addEmails(Arrays.asList(\n                new InternetAddress(\"support@example.com\"),\n                new InternetAddress(\"help@example.com\"))\n        );\n\n        builder.setCountry(\"XX\");\n        builder.setLocality(\"Testville\");\n        builder.setOrganization(\"Testing Co\");\n        builder.setOrganizationalUnit(\"Testunit\");\n        builder.setState(\"ABC\");\n\n        assertThat(builder.toString()).isEqualTo(\"CN=mail@example.com,C=XX,L=Testville,\"\n                + \"O=Testing Co,OU=Testunit,ST=ABC,\"\n                + \"EMAIL=mail@example.com,EMAIL=info@example.com,\"\n                + \"EMAIL=sales@example.com,EMAIL=shop@example.com,\"\n                + \"EMAIL=support@example.com,EMAIL=help@example.com,\"\n                + \"TYPE=SIGNING_AND_ENCRYPTION\");\n\n        builder.sign(testKey);\n\n        var csr = builder.getCSR();\n        assertThat(csr).isNotNull();\n        assertThat(csr.getEncoded()).isEqualTo(builder.getEncoded());\n\n        smimeCsrTest(csr);\n        keyUsageTest(csr, KeyUsage.digitalSignature | KeyUsage.keyEncipherment);\n        writerTest(builder);\n    }\n\n    /**\n     * Test if the generated S/MIME CSR correctly sets the encryption only flag.\n     */\n    @Test\n    public void testSMIMEEncryptOnly() throws IOException, AddressException {\n        var builder = new SMIMECSRBuilder();\n        builder.addEmail(new InternetAddress(\"mail@example.com\"));\n        builder.setKeyUsageType(KeyUsageType.ENCRYPTION_ONLY);\n        builder.sign(testKey);\n        var csr = builder.getCSR();\n        assertThat(csr).isNotNull();\n        keyUsageTest(csr, KeyUsage.keyEncipherment);\n    }\n\n    /**\n     * Test if the generated S/MIME CSR correctly sets the signing only flag.\n     */\n    @Test\n    public void testSMIMESigningOnly() throws IOException, AddressException {\n        var builder = new SMIMECSRBuilder();\n        builder.addEmail(new InternetAddress(\"mail@example.com\"));\n        builder.setKeyUsageType(KeyUsageType.SIGNING_ONLY);\n        builder.sign(testKey);\n        var csr = builder.getCSR();\n        assertThat(csr).isNotNull();\n        keyUsageTest(csr, KeyUsage.digitalSignature);\n    }\n\n    /**\n     * Test if the generated S/MIME CSR correctly sets the signing and encryption flag.\n     */\n    @Test\n    public void testSMIMESigningAndEncryption() throws IOException, AddressException {\n        var builder = new SMIMECSRBuilder();\n        builder.addEmail(new InternetAddress(\"mail@example.com\"));\n        builder.setKeyUsageType(KeyUsageType.SIGNING_AND_ENCRYPTION);\n        builder.sign(testKey);\n        var csr = builder.getCSR();\n        assertThat(csr).isNotNull();\n        keyUsageTest(csr, KeyUsage.digitalSignature | KeyUsage.keyEncipherment);\n    }\n\n    /**\n     * Checks that addValue behaves correctly in dependence of the attributes being added.\n     * If a common name is set, it should be handled in the same way when it's added by\n     * using {@link SMIMECSRBuilder#addEmail(InternetAddress)}.\n     */\n    @Test\n    public void testAddAttrValues() throws Exception {\n        var builder = new SMIMECSRBuilder();\n        var invAttNameExMessage = assertThrows(IllegalArgumentException.class,\n                () -> X500Name.getDefaultStyle().attrNameToOID(\"UNKNOWNATT\")).getMessage();\n\n        assertThat(builder.toString()).isEqualTo(\",TYPE=SIGNING_AND_ENCRYPTION\");\n\n        assertThatNullPointerException()\n                .as(\"addValue(String, String)\")\n                .isThrownBy(() -> new SMIMECSRBuilder().addValue((String) null, \"value\"));\n        assertThatNullPointerException()\n                .as(\"addValue(ASN1ObjectIdentifier, String)\")\n                .isThrownBy(() -> new SMIMECSRBuilder().addValue((ASN1ObjectIdentifier) null, \"value\"));\n        assertThatNullPointerException()\n                .as(\"addValue(String, null)\")\n                .isThrownBy(() -> new SMIMECSRBuilder().addValue(\"C\", null));\n        assertThatIllegalArgumentException()\n                .as(\"addValue(String, null)\")\n                .isThrownBy(() -> new SMIMECSRBuilder().addValue(\"UNKNOWNATT\", \"val\"))\n                .withMessage(invAttNameExMessage);\n        assertThatExceptionOfType(AddressException.class)\n                .as(\"addValue(String, invalid String)\")\n                .isThrownBy(() -> new SMIMECSRBuilder().addValue(\"CN\", \"invalid@example..com\"));\n\n        assertThat(builder.toString()).isEqualTo(\",TYPE=SIGNING_AND_ENCRYPTION\");\n\n        builder.addValue(\"C\", \"DE\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,TYPE=SIGNING_AND_ENCRYPTION\");\n        builder.addValue(\"E\", \"contact@example.com\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,E=contact@example.com,TYPE=SIGNING_AND_ENCRYPTION\");\n        builder.addValue(\"CN\", \"firstcn@example.com\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,E=contact@example.com,CN=firstcn@example.com,EMAIL=firstcn@example.com,TYPE=SIGNING_AND_ENCRYPTION\");\n        builder.addValue(\"CN\", \"scnd@example.com\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,E=contact@example.com,CN=firstcn@example.com,EMAIL=firstcn@example.com,EMAIL=scnd@example.com,TYPE=SIGNING_AND_ENCRYPTION\");\n\n        builder = new SMIMECSRBuilder();\n        builder.addValue(BCStyle.C, \"DE\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,TYPE=SIGNING_AND_ENCRYPTION\");\n        builder.addValue(BCStyle.EmailAddress, \"contact@example.com\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,E=contact@example.com,TYPE=SIGNING_AND_ENCRYPTION\");\n        builder.addValue(BCStyle.CN, \"firstcn@example.com\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,E=contact@example.com,CN=firstcn@example.com,EMAIL=firstcn@example.com,TYPE=SIGNING_AND_ENCRYPTION\");\n        builder.addValue(BCStyle.CN, \"scnd@example.com\");\n        assertThat(builder.toString()).isEqualTo(\"C=DE,E=contact@example.com,CN=firstcn@example.com,EMAIL=firstcn@example.com,EMAIL=scnd@example.com,TYPE=SIGNING_AND_ENCRYPTION\");\n    }\n\n    /**\n     * Checks if the S/MIME CSR contains the right parameters.\n     * <p>\n     * This is not supposed to be a Bouncy Castle test. If the\n     * {@link PKCS10CertificationRequest} contains the right parameters, we assume that\n     * Bouncy Castle encodes it properly.\n     */\n    private void smimeCsrTest(PKCS10CertificationRequest csr) {\n        var name = csr.getSubject();\n\n        try (var softly = new AutoCloseableSoftAssertions()) {\n            softly.assertThat(name.getRDNs(BCStyle.CN)).as(\"CN\")\n                    .extracting(rdn -> rdn.getFirst().getValue().toString())\n                    .contains(\"mail@example.com\");\n            softly.assertThat(name.getRDNs(BCStyle.C)).as(\"C\")\n                    .extracting(rdn -> rdn.getFirst().getValue().toString())\n                    .contains(\"XX\");\n            softly.assertThat(name.getRDNs(BCStyle.L)).as(\"L\")\n                    .extracting(rdn -> rdn.getFirst().getValue().toString())\n                    .contains(\"Testville\");\n            softly.assertThat(name.getRDNs(BCStyle.O)).as(\"O\")\n                    .extracting(rdn -> rdn.getFirst().getValue().toString())\n                    .contains(\"Testing Co\");\n            softly.assertThat(name.getRDNs(BCStyle.OU)).as(\"OU\")\n                    .extracting(rdn -> rdn.getFirst().getValue().toString())\n                    .contains(\"Testunit\");\n            softly.assertThat(name.getRDNs(BCStyle.ST)).as(\"ST\")\n                    .extracting(rdn -> rdn.getFirst().getValue().toString())\n                    .contains(\"ABC\");\n        }\n\n        var attr = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);\n        assertThat(attr).hasSize(1);\n\n        var extensions = attr[0].getAttrValues().toArray();\n        assertThat(extensions).hasSize(1);\n\n        var names = GeneralNames.fromExtensions((Extensions) extensions[0], Extension.subjectAlternativeName);\n        assertThat(names.getNames())\n                .filteredOn(gn -> gn.getTagNo() == GeneralName.rfc822Name)\n                .extracting(gn -> ASN1IA5String.getInstance(gn.getName()).getString())\n                .containsExactlyInAnyOrder(\"mail@example.com\", \"info@example.com\",\n                        \"sales@example.com\", \"shop@example.com\", \"support@example.com\",\n                        \"help@example.com\");\n    }\n\n    /**\n     * Validate the Key Usage bits.\n     *\n     * @param csr\n     *         {@link PKCS10CertificationRequest} to validate\n     * @param expectedUsageBits\n     *         Expected key usage bits. Exact match, validation fails if other bits are\n     *         set or reset. If {@code null}, validation fails if key usage bits are set.\n     */\n    private void keyUsageTest(PKCS10CertificationRequest csr, Integer expectedUsageBits) {\n        var attr = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);\n        assertThat(attr).hasSize(1);\n        var extensions = attr[0].getAttrValues().toArray();\n        assertThat(extensions).hasSize(1);\n        var keyUsageBits = (DERBitString) ((Extensions) extensions[0]).getExtensionParsedValue(Extension.keyUsage);\n        if (expectedUsageBits != null) {\n            assertThat(keyUsageBits.intValue()).isEqualTo(expectedUsageBits);\n        } else {\n            assertThat(keyUsageBits).isNull();\n        }\n    }\n\n    /**\n     * Checks if the {@link SMIMECSRBuilder#write(java.io.Writer)} method generates a\n     * correct CSR PEM file.\n     */\n    private void writerTest(SMIMECSRBuilder builder) throws IOException {\n        // Write CSR to PEM\n        String pem;\n        try (var out = new StringWriter()) {\n            builder.write(out);\n            pem = out.toString();\n        }\n\n        // Make sure PEM file is properly formatted\n        assertThat(pem).matches(\n                  \"-----BEGIN CERTIFICATE REQUEST-----[\\\\r\\\\n]+\"\n                + \"([a-zA-Z0-9/+=]+[\\\\r\\\\n]+)+\"\n                + \"-----END CERTIFICATE REQUEST-----[\\\\r\\\\n]*\");\n\n        // Read CSR from PEM\n        PKCS10CertificationRequest readCsr;\n        try (var parser = new PEMParser(new StringReader(pem))) {\n            readCsr = (PKCS10CertificationRequest) parser.readObject();\n        }\n\n        // Verify that both keypairs are the same\n        assertThat(builder.getCSR()).isNotSameAs(readCsr);\n        assertThat(builder.getEncoded()).isEqualTo(readCsr.getEncoded());\n\n        // OutputStream is identical?\n        byte[] pemBytes;\n        try (var baos = new ByteArrayOutputStream()) {\n            builder.write(baos);\n            pemBytes = baos.toByteArray();\n        }\n        assertThat(new String(pemBytes, UTF_8)).isEqualTo(pem);\n    }\n\n    /**\n     * Make sure an exception is thrown when nothing is set.\n     */\n    @Test\n    public void testNoEmail() {\n        assertThrows(IllegalStateException.class, () -> {\n            var builder = new SMIMECSRBuilder();\n            builder.sign(testKey);\n        });\n    }\n\n    /**\n     * Make sure all getters will fail if the CSR is not signed.\n     */\n    @Test\n    public void testNoSign() {\n        var builder = new SMIMECSRBuilder();\n\n        assertThrows(IllegalStateException.class, builder::getCSR, \"getCSR\");\n        assertThrows(IllegalStateException.class, builder::getEncoded, \"getEncoded\");\n        assertThrows(IllegalStateException.class, () -> {\n            try (var w = new StringWriter()) {\n                builder.write(w);\n            }\n        },\"write\");\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/test/java/org/shredzone/acme4j/smime/email/EmailProcessorTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2021 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.email;\n\nimport static jakarta.mail.Message.RecipientType.TO;\nimport static org.assertj.core.api.Assertions.*;\n\nimport java.io.IOException;\nimport java.security.KeyStore;\nimport java.security.Security;\nimport java.util.Optional;\n\nimport jakarta.mail.Message;\nimport jakarta.mail.MessagingException;\nimport jakarta.mail.internet.InternetAddress;\nimport org.bouncycastle.jce.provider.BouncyCastleProvider;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.Identifier;\nimport org.shredzone.acme4j.exception.AcmeProtocolException;\nimport org.shredzone.acme4j.smime.EmailIdentifier;\nimport org.shredzone.acme4j.smime.SMIMETests;\nimport org.shredzone.acme4j.smime.exception.AcmeInvalidMessageException;\n\n/**\n * Unit tests for {@link EmailProcessor} and {@link ResponseGenerator}.\n */\npublic class EmailProcessorTest extends SMIMETests {\n\n    private final InternetAddress expectedFrom = email(\"acme-generator@example.org\");\n    private final InternetAddress expectedTo = email(\"alexey@example.com\");\n    private final InternetAddress expectedReplyTo = email(\"acme-validator@example.org\");\n    private final Message message = mockMessage(\"challenge\");\n\n    @BeforeAll\n    public static void setup() {\n        Security.addProvider(new BouncyCastleProvider());\n    }\n\n    @Test\n    public void testEmailParser() throws AcmeInvalidMessageException {\n        var processor = EmailProcessor.plainMessage(message);\n        processor.expectedFrom(expectedFrom);\n        processor.expectedTo(expectedTo);\n        processor.expectedIdentifier(EmailIdentifier.email(expectedTo));\n        processor.expectedIdentifier(new Identifier(\"email\", expectedTo.getAddress()));\n\n        assertThat(processor.getSender()).isEqualTo(expectedFrom);\n        assertThat(processor.getRecipient()).isEqualTo(expectedTo);\n        assertThat(processor.getMessageId()).isEqualTo(Optional.of(\"<A2299BB.FF7788@example.org>\"));\n        assertThat(processor.getToken1()).isEqualTo(TOKEN_PART1);\n        assertThat(processor.getReplyTo()).contains(email(\"acme-validator@example.org\"));\n    }\n\n    @Test\n    public void testValidSignature() {\n        assertThatNoException().isThrownBy(() -> {\n            var message = mockMessage(\"valid-mail\");\n            var certificate = readCertificate(\"valid-signer\");\n            EmailProcessor.builder().certificate(certificate).strict().build(message);\n        });\n    }\n\n    @Test\n    public void testInvalidSignature() {\n        var ex = catchThrowableOfType(() -> {\n                    var message = mockMessage(\"invalid-signed-mail\");\n                    var certificate = readCertificate(\"valid-signer\");\n                    EmailProcessor.builder().certificate(certificate).strict().build(message);\n                }, AcmeInvalidMessageException.class);\n\n        assertThat(ex).isNotNull();\n        assertThat(ex.getMessage()).isEqualTo(\"Invalid signature\");\n        assertThat(ex.getErrors()).hasSize(2);\n        assertThat(ex.getErrors())\n                .first().hasFieldOrPropertyWithValue(\"id\", \"SignedMailValidator.emailFromCertMismatch\");\n        assertThat(ex.getErrors())\n                .element(1).hasFieldOrPropertyWithValue(\"id\", \"SignedMailValidator.certPathInvalid\");\n    }\n\n    @Test\n    public void testValidSignatureButNoSAN() {\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(() -> {\n                    var message = mockMessage(\"invalid-nosan\");\n                    var certificate = readCertificate(\"valid-signer-nosan\");\n                    EmailProcessor.builder().certificate(certificate).strict().build(message);\n                })\n                .withMessage(\"Certificate does not have a subjectAltName extension\");\n    }\n\n    @Test\n    public void testSANDoesNotMatchFrom() {\n        var ex = catchThrowableOfType(() -> {\n                    var message = mockMessage(\"invalid-cert-mismatch\");\n                    var certificate = readCertificate(\"valid-signer\");\n                    EmailProcessor.builder().certificate(certificate).strict().build(message);\n                }, AcmeInvalidMessageException.class);\n\n        assertThat(ex).isNotNull();\n        assertThat(ex.getMessage()).isEqualTo(\"Invalid signature\");\n        assertThat(ex.getErrors())\n                .singleElement().hasFieldOrPropertyWithValue(\"id\", \"SignedMailValidator.emailFromCertMismatch\");\n    }\n\n    @Test\n    public void testInvalidProtectedFromHeader() {\n        var ex = catchThrowableOfType(() -> {\n                    var message = mockMessage(\"invalid-protected-mail-from\");\n                    var certificate = readCertificate(\"valid-signer\");\n                    EmailProcessor.builder().certificate(certificate).strict().build(message);\n                }, AcmeInvalidMessageException.class);\n\n        assertThat(ex).isNotNull();\n        assertThat(ex.getMessage()).isEqualTo(\"Invalid signature\");\n        assertThat(ex.getErrors())\n                .singleElement().hasFieldOrPropertyWithValue(\"id\", \"SignedMailValidator.emailFromCertMismatch\");\n    }\n\n    @Test\n    public void testInvalidProtectedToHeader() {\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(() -> {\n                    var message = mockMessage(\"invalid-protected-mail-to\");\n                    var certificate = readCertificate(\"valid-signer\");\n                    EmailProcessor.builder().certificate(certificate).strict().build(message);\n                })\n                .withMessage(\"Secured header 'To' does not match envelope header\");\n    }\n\n    @Test\n    public void testInvalidProtectedSubjectHeader() {\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(() -> {\n                    var message = mockMessage(\"invalid-protected-mail-subject\");\n                    var certificate = readCertificate(\"valid-signer\");\n                    EmailProcessor.builder().certificate(certificate).strict().build(message);\n                })\n                .withMessage(\"Secured header 'Subject' does not match envelope header\");\n    }\n\n    @Test\n    public void testNonStrictInvalidProtectedSubjectHeader() {\n        assertThatNoException()\n                .isThrownBy(() -> {\n                    var message = mockMessage(\"invalid-protected-mail-subject\");\n                    var certificate = readCertificate(\"valid-signer\");\n                    EmailProcessor.builder().certificate(certificate).relaxed().build(message);\n                });\n    }\n\n    // TODO: This test is blocking development atm. It fails because the signature of\n    // the test email has expired. In order to fix it, RFC-7508 compliant test emails\n    // need to be generated programmatically within the unit test.\n    @Disabled\n    @Test\n    public void testValidSignatureRfc7508() throws Exception {\n        var message = mockMessage(\"valid-mail-7508\");\n\n        var keyStore = KeyStore.getInstance(\"JKS\");\n        keyStore.load(EmailProcessorTest.class.getResourceAsStream(\"/7508-valid-ca.jks\"), \"test123\".toCharArray());\n\n        var processor = EmailProcessor.builder().trustStore(keyStore).build(message);\n        assertThat(processor.getSender()).isEqualTo(new InternetAddress(\"acme-challenge@dc-bsd.my.corp\"));\n        assertThat(processor.getRecipient()).isEqualTo(new InternetAddress(\"gitlab@dc-bsd.my.corp\"));\n        assertThat(processor.getToken1()).isEqualTo(\"ABxfL5s4bjvmyVRvl6y-Y_GhdzTdWpKqlmrKAIVe\");\n    }\n\n    // TODO: This test is blocking development atm. It fails because the keystore format\n    // is invalid. This might be fixed together with testValidSignatureRfc7508().\n    @Disabled\n    @Test\n    public void testInvalidSignatureRfc7508() throws Exception {\n        var message = mockMessage(\"valid-mail-7508\");\n\n        var keyStore = KeyStore.getInstance(\"JKS\");\n        keyStore.load(EmailProcessorTest.class.getResourceAsStream(\"/7508-fake-ca.jks\"), \"test123\".toCharArray());\n\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(() -> EmailProcessor.builder().trustStore(keyStore).build(message));\n    }\n\n    @Test\n    public void textExpectedFromFails() {\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(() -> {\n                    var processor = EmailProcessor.plainMessage(message);\n                    processor.expectedFrom(expectedTo);\n                })\n                .withMessage(\"Message is not sent by the expected sender\");\n    }\n\n    @Test\n    public void textExpectedToFails() {\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(() -> {\n                    var processor = EmailProcessor.plainMessage(message);\n                    processor.expectedTo(expectedFrom);\n                })\n                .withMessage(\"Message is not addressed to expected recipient\");\n    }\n\n    @Test\n    public void textExpectedIdentifierFails1() {\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(() -> {\n                    var processor = EmailProcessor.plainMessage(message);\n                    processor.expectedIdentifier(EmailIdentifier.email(expectedFrom));\n                })\n                .withMessage(\"Message is not addressed to expected recipient\");\n    }\n\n    @Test\n    public void textExpectedIdentifierFails2() {\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(() -> {\n                    var processor = EmailProcessor.plainMessage(message);\n                    processor.expectedIdentifier(Identifier.ip(\"192.0.2.1\"));\n                })\n                .withMessage(\"Wrong identifier type: ip\");\n    }\n\n    @Test\n    public void textNoChallengeFails1() {\n        assertThatIllegalStateException()\n                .isThrownBy(() -> {\n                    var processor = EmailProcessor.plainMessage(message);\n                    processor.getToken();\n                })\n                .withMessage(\"No challenge has been set yet\");\n    }\n\n    @Test\n    public void textNoChallengeFails2() {\n        assertThatIllegalStateException()\n                .isThrownBy(() -> {\n                    var processor = EmailProcessor.plainMessage(message);\n                    processor.getAuthorization();\n                })\n                .withMessage(\"No challenge has been set yet\");\n    }\n\n    @Test\n    public void textNoChallengeFails3() {\n        assertThatIllegalStateException()\n                .isThrownBy(() -> {\n                    var processor = EmailProcessor.plainMessage(message);\n                    processor.respond();\n                })\n                .withMessage(\"No challenge has been set yet\");\n    }\n\n    @Test\n    public void testChallenge() throws AcmeInvalidMessageException {\n        var challenge = mockChallenge(\"emailReplyChallenge\");\n\n        var processor = EmailProcessor.plainMessage(message);\n        processor.withChallenge(challenge);\n        assertThat(processor.getToken()).isEqualTo(TOKEN);\n        assertThat(processor.getAuthorization()).isEqualTo(KEY_AUTHORIZATION);\n        assertThat(processor.respond()).isNotNull();\n    }\n\n    @Test\n    public void testChallengeMismatch() {\n        assertThatExceptionOfType(AcmeProtocolException.class)\n                .isThrownBy(() -> {\n                    var challenge = mockChallenge(\"emailReplyChallengeMismatch\");\n                    var processor = EmailProcessor.plainMessage(message);\n                    processor.withChallenge(challenge);\n                })\n                .withMessage(\"Message is not sent by the expected sender\");\n    }\n\n    @Test\n    public void testResponse() throws IOException, MessagingException, AcmeInvalidMessageException {\n        var challenge = mockChallenge(\"emailReplyChallenge\");\n\n        var response = EmailProcessor.plainMessage(message)\n                .withChallenge(challenge)\n                .respond()\n                .generateResponse(mailSession);\n\n        assertResponse(response, RESPONSE_BODY);\n    }\n\n    @Test\n    public void testResponseWithHeaderFooter() throws IOException, MessagingException, AcmeInvalidMessageException {\n        var challenge = mockChallenge(\"emailReplyChallenge\");\n\n        var response = EmailProcessor.plainMessage(message)\n                .withChallenge(challenge)\n                .respond()\n                .withHeader(\"This is an introduction.\")\n                .withFooter(\"This is a footer.\")\n                .generateResponse(mailSession);\n\n        assertResponse(response,\n                \"This is an introduction.\\r\\n\"\n                + RESPONSE_BODY\n                + \"This is a footer.\");\n    }\n\n    @Test\n    public void testResponseWithCallback() throws IOException, MessagingException, AcmeInvalidMessageException {\n        var challenge = mockChallenge(\"emailReplyChallenge\");\n\n        var response = EmailProcessor.plainMessage(message)\n                .withChallenge(challenge)\n                .respond()\n                .withGenerator((msg, body) -> msg.setContent(\"Head\\r\\n\" + body + \"Foot\", \"text/plain\"))\n                .generateResponse(mailSession);\n\n        assertResponse(response, \"Head\\r\\n\" + RESPONSE_BODY + \"Foot\");\n    }\n\n    private void assertResponse(Message response, String expectedBody)\n            throws MessagingException, IOException {\n        assertThat(response.getContentType()).isEqualTo(\"text/plain\");\n        assertThat(response.getContent().toString()).isEqualTo(expectedBody);\n\n        // This is a response, so the expected sender is the recipient of the challenge\n        assertThat(response.getFrom()).hasSize(1);\n        assertThat(response.getFrom()[0]).isEqualTo(expectedTo);\n\n        // There is a Reply-To header, so we expect the mail to go only there\n        assertThat(response.getRecipients(TO)).hasSize(1);\n        assertThat(response.getRecipients(TO)[0]).isEqualTo(expectedReplyTo);\n\n        assertThat(response.getSubject()).isEqualTo(\"Re: ACME: \" + TOKEN_PART1);\n\n        var inReplyToHeader = response.getHeader(\"In-Reply-To\");\n        assertThat(inReplyToHeader).hasSize(1);\n        assertThat(inReplyToHeader[0]).isEqualTo(\"<A2299BB.FF7788@example.org>\");\n    }\n\n}\n"
  },
  {
    "path": "acme4j-smime/src/test/java/org/shredzone/acme4j/smime/wrapper/SignedMailBuilderTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2023 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.wrapper;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.security.KeyStoreException;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for {@link SignedMailBuilder}.\n */\npublic class SignedMailBuilderTest {\n\n    @Test\n    public void testDefaultTrustStoreIsCreated() throws KeyStoreException {\n        var keyStore = SignedMailBuilder.getCaCertsTrustStore();\n        assertThat(keyStore).isNotNull();\n        assertThat(keyStore.size()).isGreaterThan(0);\n\n        // Make sure the instance is cached\n        var keyStore2 = SignedMailBuilder.getCaCertsTrustStore();\n        assertThat(keyStore2).isSameAs(keyStore);\n    }\n\n}"
  },
  {
    "path": "acme4j-smime/src/test/java/org/shredzone/acme4j/smime/wrapper/SignedMailTest.java",
    "content": "/*\n * acme4j - Java ACME client\n *\n * Copyright (C) 2023 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.acme4j.smime.wrapper;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatExceptionOfType;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Enumeration;\n\nimport jakarta.mail.Header;\nimport jakarta.mail.internet.InternetAddress;\nimport org.junit.jupiter.api.Test;\nimport org.shredzone.acme4j.smime.exception.AcmeInvalidMessageException;\n\n/**\n * Unit tests for {@link SignedMail}.\n */\npublic class SignedMailTest {\n\n    @Test\n    public void testCheckDuplicatedStrictGood() throws Exception {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"From\", \"foo@example.com\"\n        ));\n\n        // Success: Field is present and identical\n        signedMail.checkDuplicatedField(\"From\", \"foo@example.com\", false);\n\n        assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress(\"foo@example.com\"));\n    }\n\n    @Test\n    public void testCheckDuplicatedStrictBad() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"From\", \"  foo@example.com \"\n        ));\n\n        // Failure: Field is same, but has extra whitespaces\n        assertThatExceptionOfType(AcmeInvalidMessageException.class).isThrownBy(() ->\n                signedMail.checkDuplicatedField(\"From\", \"foo@example.com\", false)\n        );\n    }\n\n    @Test\n    public void testCheckDuplicatedRelaxedGood() throws Exception {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"FROM\", \"  foo@example.com \"\n        ));\n\n        // Good: Field is there and identical (ignoring case and whitespaces)\n        signedMail.checkDuplicatedField(\"From\", \"foo@example.com\", true);\n\n        assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress(\"foo@example.com\"));\n    }\n\n    @Test\n    public void testCheckDuplicatedRelaxedBad() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"From\", \"bar@example.com\"\n        ));\n\n        // Failure: Field is present, but different value\n        assertThatExceptionOfType(AcmeInvalidMessageException.class).isThrownBy(() ->\n                signedMail.checkDuplicatedField(\"From\", \"foo@example.com\", true)\n        );\n    }\n\n    @Test\n    public void testDeleteFieldStrictGood() throws Exception {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"From\", \"foo@example.com\"\n        ));\n\n        // Good: Field is present and identical\n        signedMail.deleteField(\"From\", \"foo@example.com\", false);\n\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(signedMail::getFrom)\n                .withMessage(\"Protected 'FROM' header is required, but missing\");\n    }\n\n    @Test\n    public void testDeleteFieldStrictBad() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"From\", \"bar@example.com\"\n        ));\n\n        // Bad: Field is present, but has different value\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(() -> signedMail.deleteField(\"From\", \"foo@example.com\", false))\n                .withMessage(\"Secured header 'From' was not found in envelope header for deletion\");\n    }\n\n    @Test\n    public void testDeleteFieldRelaxedGood() throws Exception {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"FROM\", \"   foo@example.com \"\n        ));\n\n        // Good: Field is present and identical (ignoring case and whitespaces)\n        signedMail.deleteField(\"From\", \"foo@example.com\", true);\n\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(signedMail::getFrom)\n                .withMessage(\"Protected 'FROM' header is required, but missing\");\n    }\n\n    @Test\n    public void testDeleteFieldRelaxedBad() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"FROM\", \"bar@example.com\"\n        ));\n\n        // Bad: Field is present, but has different value\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(() -> signedMail.deleteField(\"From\", \"foo@example.com\", true))\n                .withMessage(\"Secured header 'From' was not found in envelope header for deletion\");\n    }\n\n    @Test\n    public void testModifyFieldStrictGood() throws Exception {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"From\", \"foo@example.com\"\n        ));\n\n        // Good: field is present, content is replaced\n        signedMail.modifyField(\"From\", \"bar@example.com\", false);\n\n        assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress(\"bar@example.com\"));\n    }\n\n    @Test\n    public void testModifyFieldStrictBad() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"FROM\", \"foo@example.com\"\n        ));\n\n        // Failure: Field is not present because it's all-caps\n        assertThatExceptionOfType(AcmeInvalidMessageException.class).isThrownBy(() ->\n                signedMail.modifyField(\"From\", \"bar@example.com\", false)\n        );\n    }\n\n    @Test\n    public void testModifyFieldRelaxedGood() throws Exception {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"FROM\", \"foo@example.com\"\n        ));\n\n        // Good: Field is present (ignoring case)\n        signedMail.modifyField(\"From\", \"bar@example.com\", true);\n\n        assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress(\"bar@example.com\"));\n    }\n\n    @Test\n    public void testModifyFieldRelaxedBad() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"To\", \"foo@example.com\"\n        ));\n\n        // Failure: Field is not present at all\n        assertThatExceptionOfType(AcmeInvalidMessageException.class).isThrownBy(() ->\n                signedMail.modifyField(\"From\", \"foo@example.com\", true)\n        );\n    }\n\n    @Test\n    public void testImportUntrusted() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"From\", \"foo@example.com\",\n                \"Message-Id\", \"123456ABCDEF\"\n        ));\n\n        // Success because Message ID does not need to be trusted\n        assertThat(signedMail.getMessageId()).isNotEmpty().contains(\"123456ABCDEF\");\n\n        // Failure because From is required to be trusted\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(signedMail::getFrom);\n    }\n\n    @Test\n    public void testImportTrustedStrict() throws Exception {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"From\", \"foo@example.com\",\n                \"Message-Id\", \"123456ABCDEF\"\n        ));\n        signedMail.importTrustedHeaders(withHeaders(\n                \"From\", \"foo@example.com\"\n        ));\n\n        // Success because Message ID does not need to be trusted\n        assertThat(signedMail.getMessageId()).isNotEmpty().contains(\"123456ABCDEF\");\n\n        // Success because From is trusted\n        assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress(\"foo@example.com\"));\n    }\n\n    @Test\n    public void testImportTrustedRelaxed() throws Exception {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"Message-Id\", \"123456ABCDEF\"\n        ));\n        signedMail.importTrustedHeadersRelaxed(withHeaders(\n                \"From\", \"foo@example.com\"\n        ));\n\n        // Success because Message ID does not need to be trusted\n        assertThat(signedMail.getMessageId()).isNotEmpty().contains(\"123456ABCDEF\");\n\n        // Success because From is trusted\n        assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress(\"foo@example.com\"));\n    }\n\n    @Test\n    public void testImportStrictFails() {\n        var signedMail = new SignedMail();\n\n        // Fails because there is no matching untrusted header\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(() -> signedMail.importTrustedHeaders(withHeaders(\n                        \"From\", \"foo@example.com\"\n                )));\n    }\n\n    @Test\n    public void testFromEmpty() {\n        var signedMail = new SignedMail();\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(signedMail::getFrom)\n                .withMessage(\"Protected 'FROM' header is required, but missing\");\n    }\n\n    @Test\n    public void testFromUntrusted() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"From\", \"foo@example.com\"\n        ));\n\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(signedMail::getFrom)\n                .withMessage(\"Protected 'FROM' header is required, but missing\");\n    }\n\n    @Test\n    public void testFromTrusted() throws Exception {\n        var signedMail = new SignedMail();\n        signedMail.importTrustedHeadersRelaxed(withHeaders(\n                \"From\", \"foo@example.com\"\n        ));\n\n        assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress(\"foo@example.com\"));\n    }\n\n    @Test\n    public void testToEmpty() {\n        var signedMail = new SignedMail();\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(signedMail::getTo)\n                .withMessage(\"Protected 'TO' header is required, but missing\");\n    }\n\n    @Test\n    public void testToUntrusted() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"To\", \"foo@example.com\"\n        ));\n\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(signedMail::getTo)\n                .withMessage(\"Protected 'TO' header is required, but missing\");\n    }\n\n    @Test\n    public void testToTrusted() throws Exception {\n        var signedMail = new SignedMail();\n        signedMail.importTrustedHeadersRelaxed(withHeaders(\n                \"To\", \"foo@example.com\"\n        ));\n\n        assertThat(signedMail.getTo()).isEqualTo(new InternetAddress(\"foo@example.com\"));\n    }\n\n    @Test\n    public void testSubjectEmpty() {\n        var signedMail = new SignedMail();\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(signedMail::getSubject)\n                .withMessage(\"Protected 'SUBJECT' header is required, but missing\");\n    }\n\n    @Test\n    public void testSubjectUntrusted() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"Subject\", \"abc123\"\n        ));\n\n        assertThatExceptionOfType(AcmeInvalidMessageException.class)\n                .isThrownBy(signedMail::getSubject)\n                .withMessage(\"Protected 'SUBJECT' header is required, but missing\");\n    }\n\n    @Test\n    public void testSubjectTrusted() throws Exception {\n        var signedMail = new SignedMail();\n        signedMail.importTrustedHeadersRelaxed(withHeaders(\n                \"Subject\", \"abc123\"\n        ));\n\n        assertThat(signedMail.getSubject()).isEqualTo(\"abc123\");\n    }\n\n    @Test\n    public void testMessageIdEmpty() {\n        var signedMail = new SignedMail();\n        assertThat(signedMail.getMessageId()).isEmpty();\n    }\n\n    @Test\n    public void testMessageId() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"Message-Id\", \"12345ABCDE\"\n        ));\n\n        assertThat(signedMail.getMessageId()).isNotEmpty().contains(\"12345ABCDE\");\n    }\n\n    @Test\n    public void testReplyToEmpty() throws Exception {\n        var signedMail = new SignedMail();\n        assertThat(signedMail.getReplyTo()).isEmpty();\n    }\n\n    @Test\n    public void testReplyTo() throws Exception {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"Reply-To\", \"foo@example.com\",\n                \"Reply-To\", \"bar@example.org\"\n        ));\n\n        assertThat(signedMail.getReplyTo()).contains(\n                new InternetAddress(\"foo@example.com\"),\n                new InternetAddress(\"bar@example.org\")\n        );\n    }\n\n    @Test\n    public void testIsAutoSubmitted() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"Auto-Submitted\", \"auto-generated; type=acme\"\n        ));\n\n        assertThat(signedMail.isAutoSubmitted()).isTrue();\n    }\n\n    @Test\n    public void testIsNotAutoSubmitted() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"Auto-Submitted\", \"no\"\n        ));\n\n        assertThat(signedMail.isAutoSubmitted()).isFalse();\n    }\n\n    @Test\n    public void testIsAutoSubmittedMissing() {\n        var signedMail = new SignedMail();\n        assertThat(signedMail.isAutoSubmitted()).isFalse();\n    }\n\n    @Test\n    public void testMissingSecuredHeadersEmpty() {\n        var signedMail = new SignedMail();\n        assertThat(signedMail.getMissingSecuredHeaders()).contains(\"FROM\", \"TO\", \"SUBJECT\");\n    }\n\n    @Test\n    public void testMissingSecuredHeadersGood() {\n        var signedMail = new SignedMail();\n        signedMail.importTrustedHeadersRelaxed(withHeaders(\n                \"From\", \"foo@example.com\",\n                \"To\", \"bar@example.org\",\n                \"Subject\", \"foo123\"\n        ));\n\n        assertThat(signedMail.getMissingSecuredHeaders()).isEmpty();\n    }\n\n    @Test\n    public void testMissingSecuredHeadersTrustedBad() {\n        var signedMail = new SignedMail();\n        signedMail.importTrustedHeadersRelaxed(withHeaders(\n                \"From\", \"foo@example.com\",\n                \"To\", \"bar@example.org\"\n        ));\n\n        assertThat(signedMail.getMissingSecuredHeaders()).contains(\"SUBJECT\");\n    }\n\n    @Test\n    public void testMissingSecuredHeadersUntustedBad() {\n        var signedMail = new SignedMail();\n        signedMail.importUntrustedHeaders(withHeaders(\n                \"From\", \"foo@example.com\",\n                \"To\", \"bar@example.org\",\n                \"Subject\", \"foo123\"\n        ));\n\n        assertThat(signedMail.getMissingSecuredHeaders()).contains(\"FROM\", \"TO\", \"SUBJECT\");\n    }\n\n    private Enumeration<Header> withHeaders(String... kv) {\n        var headers = new ArrayList<Header>();\n        for (var ix = 0; ix < kv.length; ix += 2) {\n            headers.add(new Header(kv[ix], kv[ix+1]));\n        }\n        return Collections.enumeration(headers);\n    }\n\n}"
  },
  {
    "path": "acme4j-smime/src/test/resources/.gitignore",
    "content": ""
  },
  {
    "path": "acme4j-smime/src/test/resources/email/challenge.eml",
    "content": "Auto-Submitted: auto-generated; type=acme\r\nDate: Sat, 5 Dec 2020 10:08:55 +0100\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nFrom: acme-generator@example.org\r\nTo: Alexey <alexey@example.com>\r\nReply-To: acme-validator@example.org\r\nSubject: =?UTF-8?Q?ACME:_LgYemJLy3F1LDki=20JrdIGbEzyFJyOyf=20=206vBdyZ1TG3sME=3D?=\r\nContent-Type: text/plain\r\nMIME-Version: 1.0\r\n\r\nThis is an automatically generated ACME challenge for email address\r\n\"alexey@example.com\". If you haven't requested an S/MIME\r\ncertificate generation for this email address, be very afraid.\r\nIf you did request it, your email client might be able to process\r\nthis request automatically, or you might have to paste the first\r\ntoken part into an external program.\r\n\r\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/email/invalid-cert-mismatch.eml",
    "content": "From: different-ca@example.org\r\nTo: recipient@example.org\r\nSubject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\r\nAuto-Submitted: auto-generated; type=acme\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: multipart/signed; protocol=\"application/x-pkcs7-signature\"; micalg=\"sha1\"; boundary=\"----6B6A5C5DBC60D7D16B6C08BF092D4185\"\r\n\r\nThis is an S/MIME signed message\r\n\r\n------6B6A5C5DBC60D7D16B6C08BF092D4185\r\nContent-Type: message/RFC822; forwarded=no\r\n\r\nFrom: different-ca@example.com\r\nTo: recipient@example.org\r\nSubject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nThis is an automatically generated ACME challenge.\r\n\r\n------6B6A5C5DBC60D7D16B6C08BF092D4185\r\nContent-Type: application/x-pkcs7-signature; name=\"smime.p7s\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"smime.p7s\"\r\n\r\nMIIGzQYJKoZIhvcNAQcCoIIGvjCCBroCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3\r\nDQEHAaCCA/4wggP6MIIC4qADAgECAhQoC/xUcLhcK13sGSiYxuUPf758tDANBgkq\r\nhkiG9w0BAQsFADB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w\r\nHAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu\r\nY29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjEx\r\nMDQxMjU1MzlaFw0zMjExMDExMjU1MzlaMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQH\r\nDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIG\r\nA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1w\r\nbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzTyW1VRm+mQH\r\nYU5hZWgzgrPRkRzMLlLoCamlRs5DjGf3zIpo9a/17m60YfIXBJruImUBXxa5lp0Z\r\nqaayty+nZOpS0wBZSqTLuslZ0WuyyyW2DEBNO7jLW58cMn8MfAwMYjDcxtubNb7M\r\nAG9iZRj6wn6tKCsXtYUgAIpNhyPPtDuEZ5df1ecOnvlW2vO+MwytM8DLLtwolET/\r\ntPzOXPHDiyKjij02jyJ1DlZxptKudiKaBeY1WY/W/PpS6fGskTJQc/bZPgE3OP/9\r\nY9Y1uzZTulkO3R6MlLNdLam32/ehpJvSWyfSbToyC2ejvaXoRChPFcAmTpqA0HPg\r\nh8sudiS14QIDAQABo3QwcjAdBgNVHQ4EFgQUR0gRYoOCPJYqFiPGwB141VTCXuIw\r\nHwYDVR0jBBgwFoAUR0gRYoOCPJYqFiPGwB141VTCXuIwDwYDVR0TAQH/BAUwAwEB\r\n/zAfBgNVHREEGDAWgRR2YWxpZC1jYUBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsF\r\nAAOCAQEAJDQsNTWxVLNHBrVl6p6IetzeBt64GoWp6OP5C8/HY3dlznfbrn6ZvWBr\r\nVsnc7VZ49r8r/QvRKRrljkIQuNwaW/LmRxJ1AVGGiorspmrdz0Lf2WOXnLH/+4lR\r\nq5dar5YmGqi9Mo2j5ALg/2MiO1PonuKs1eX9tLZCCeanXFexU0qaeFZelunJ6UUm\r\nBXpaGO1QfECcNnvarudosbe1ve6YGABn8MpAY+8zdYnYHPB8Pojhzvk7/8PMHoPu\r\nnjDb3lZT+b8BDmNz+GISCUSHYkdK2rWh+8wqD3T5rOtzTqAPuSNeDNocUF6wzxem\r\nHWqvP7yM3VoXYvn7FA4NCa9mE3k8MzGCApcwggKTAgEBMIGUMHwxCzAJBgNVBAYT\r\nAlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNh\r\ndGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZh\r\nbGlkLWNhQGV4YW1wbGUuY29tAhQoC/xUcLhcK13sGSiYxuUPf758tDAJBgUrDgMC\r\nGgUAoIHYMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X\r\nDTIyMTEwNDEzMzE1NVowIwYJKoZIhvcNAQkEMRYEFNE5fZJFFgePdLTkBjs3GMcD\r\nM5UyMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjAL\r\nBglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3\r\nDQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIB\r\nAEN+8Cgvfw/72HPSSgJROYqScFrYTCRF7HTAk22zJCVzuh21rbEiPGLJ9Sy4ak/i\r\nBXvXtkX8YJBGuVYrx6QxoF8vmcwlIPVw9Qoc2FevyfRQD19hP7rd7miZ8LWu0B8v\r\nd54mr7aD5zQADLvQGlxjKvCSM3F8HF1KQrRZrfJbEL9NRrgYD7c8ZEAvisaoEfPO\r\nvRjsj9IzKg/RWhOAmh5n591ZNKVb8k0G+5lyCSuP8m/9k0sE705sVrq4sbgIgtFB\r\nHNVLwYvxb88F//rFosFW/njsnlgFx5hjpLbwKu6Du2Sd7L1oye/xUGlKDY8yuy2m\r\npwKapV2IpD8TjsL7NZ7rJ5U=\r\n\r\n------6B6A5C5DBC60D7D16B6C08BF092D4185--\r\n\r\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/email/invalid-nosan.eml",
    "content": "From: valid-ca@example.com\r\nTo: recipient@example.org\r\nSubject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\r\nAuto-Submitted: auto-generated; type=acme\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: multipart/signed; protocol=\"application/x-pkcs7-signature\"; micalg=\"sha1\"; boundary=\"----47B1F5074B8F7A13042C44F61463F58F\"\r\n\r\nThis is an S/MIME signed message\r\n\r\n------47B1F5074B8F7A13042C44F61463F58F\r\nContent-Type: message/RFC822; forwarded=no\r\n\r\nFrom: valid-ca@example.com\r\nTo: recipient@example.org\r\nSubject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nThis is an automatically generated ACME challenge.\r\n\r\n------47B1F5074B8F7A13042C44F61463F58F\r\nContent-Type: application/x-pkcs7-signature; name=\"smime.p7s\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"smime.p7s\"\r\n\r\nMIIGrAYJKoZIhvcNAQcCoIIGnTCCBpkCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3\r\nDQEHAaCCA90wggPZMIICwaADAgECAhRPShCzW2lh0D89RfDJ5mos/gMAJjANBgkq\r\nhkiG9w0BAQsFADB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w\r\nHAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu\r\nY29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjEx\r\nMDQxMjU1MzlaFw0zMjExMDExMjU1MzlaMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQH\r\nDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIG\r\nA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1w\r\nbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2BDJcOmFx6Tu\r\nW58rpO4FXbvF7ZoSpIOi4YWfSmD7LSHUq+xGx/vodijFT1Y6a74o4d+XeYTH/X4j\r\nFO9QPdzZDuG8t3ziFACctptaFywUjgYYJQGyPmfg3hn1Cz72q4tAqegOEwL78NA2\r\nYcEd6sx0udAF80C1QHi/kKBMDgj9AOyNyIZ/rBN8CZSkfkpPYWI99Fl/DOuYnr7k\r\nMN1TUWS1906mPqBslh1YVyp6fdGaL6DdlIY+ZE5c9BhST/t+7eLq7fnB5KB+tDvH\r\nD1qnL858K+5Hjfc9MUYTyffDiJaG9zHkEKi3zd0EGcaf1r+lskRqEIOqROjLSDim\r\nT5Q4CuHCYQIDAQABo1MwUTAdBgNVHQ4EFgQURoDGdRoZP6EOfLXTlNxWPDTRmp8w\r\nHwYDVR0jBBgwFoAURoDGdRoZP6EOfLXTlNxWPDTRmp8wDwYDVR0TAQH/BAUwAwEB\r\n/zANBgkqhkiG9w0BAQsFAAOCAQEAvVI9Yj8lL0cvdNY3RD/GQ/tQCGBAzoFTODJU\r\nwn4zE/LfiXfu8SxJhzcpjzKc2j+mxuwGh0OqraIO2FkpO23+X1gCdCt+ClE/6nMs\r\n8UMo4H1wMYYGhjkoLvsH9Ne5N+91PvLQG97LoLsoy+Y95ws23WyqUJ2g7A7Isk3v\r\n7MJZVH2d93hjbtWQ6+3/PP5zJwubEwiDAYvycODfvAig9+0QBIy+uE7XxnEhKxHJ\r\npvN3p8NmLya7XH3v92N7M6CioyBqw8HL7I5lt5HBqa/U9USVMMmi9v+tFLZYyd7r\r\n7acw6hB7MDcLmtEu08Cgo89K23oTm1JBJZrjZUFbYcYP+fiuizGCApcwggKTAgEB\r\nMIGUMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoM\r\nFUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xIzAh\r\nBgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1wbGUuY29tAhRPShCzW2lh0D89RfDJ\r\n5mos/gMAJjAJBgUrDgMCGgUAoIHYMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEw\r\nHAYJKoZIhvcNAQkFMQ8XDTIyMTEwNDEzMzE1NVowIwYJKoZIhvcNAQkEMRYEFK+s\r\nB97I9ZGpDGUMWLsE3dCU/pbtMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEq\r\nMAsGCWCGSAFlAwQBFjALBglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcN\r\nAwICAgCAMA0GCCqGSIb3DQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0G\r\nCSqGSIb3DQEBAQUABIIBAEvS2tccMXu/OG8fzgOWsf5MzjBdDD/6doTLuIJS7Ktv\r\n1alpT8ZqSBn4jOgrlM7efTFK8y1vlOdoFZIsfIe+92lclvgHc/2Dw4XB5SswZ59y\r\nfZH+AVtVpzi5oFYiunhBn19vRP9Bri4ma8gCRe7pUwN15Gap8gl3+UQtUY17wcME\r\nH7ALcuG0ETPTxz9p2ueN6FmrthmrDSaZVqW5nyTizgr0zSxicEcwfFz9JZGZFKyp\r\nlPSVrgCwZ1/yaWXlnBXPTdO/DmAvNUAjUk0HZFu+mnelzPPs3c5s4LY3pBNjDPE2\r\ni2hxgRjca2QMsveYdwZn8I/m1P7yatJ3EozHpO3T4Gc=\r\n\r\n------47B1F5074B8F7A13042C44F61463F58F--\r\n\r\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/email/invalid-protected-mail-from.eml",
    "content": "From: tampered-ca@example.org\r\nTo: recipient@example.org\r\nSubject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\r\nAuto-Submitted: auto-generated; type=acme\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: multipart/signed; protocol=\"application/x-pkcs7-signature\"; micalg=\"sha1\"; boundary=\"----1FD9CF28CC0AD72EF1FF6D0511838F0E\"\r\n\r\nThis is an S/MIME signed message\r\n\r\n------1FD9CF28CC0AD72EF1FF6D0511838F0E\r\nContent-Type: message/RFC822; forwarded=no\r\n\r\nFrom: valid-ca@example.com\r\nTo: recipient@example.org\r\nSubject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nThis is an automatically generated ACME challenge.\r\n\r\n------1FD9CF28CC0AD72EF1FF6D0511838F0E\r\nContent-Type: application/x-pkcs7-signature; name=\"smime.p7s\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"smime.p7s\"\r\n\r\nMIIGzQYJKoZIhvcNAQcCoIIGvjCCBroCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3\r\nDQEHAaCCA/4wggP6MIIC4qADAgECAhQoC/xUcLhcK13sGSiYxuUPf758tDANBgkq\r\nhkiG9w0BAQsFADB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w\r\nHAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu\r\nY29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjEx\r\nMDQxMjU1MzlaFw0zMjExMDExMjU1MzlaMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQH\r\nDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIG\r\nA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1w\r\nbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzTyW1VRm+mQH\r\nYU5hZWgzgrPRkRzMLlLoCamlRs5DjGf3zIpo9a/17m60YfIXBJruImUBXxa5lp0Z\r\nqaayty+nZOpS0wBZSqTLuslZ0WuyyyW2DEBNO7jLW58cMn8MfAwMYjDcxtubNb7M\r\nAG9iZRj6wn6tKCsXtYUgAIpNhyPPtDuEZ5df1ecOnvlW2vO+MwytM8DLLtwolET/\r\ntPzOXPHDiyKjij02jyJ1DlZxptKudiKaBeY1WY/W/PpS6fGskTJQc/bZPgE3OP/9\r\nY9Y1uzZTulkO3R6MlLNdLam32/ehpJvSWyfSbToyC2ejvaXoRChPFcAmTpqA0HPg\r\nh8sudiS14QIDAQABo3QwcjAdBgNVHQ4EFgQUR0gRYoOCPJYqFiPGwB141VTCXuIw\r\nHwYDVR0jBBgwFoAUR0gRYoOCPJYqFiPGwB141VTCXuIwDwYDVR0TAQH/BAUwAwEB\r\n/zAfBgNVHREEGDAWgRR2YWxpZC1jYUBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsF\r\nAAOCAQEAJDQsNTWxVLNHBrVl6p6IetzeBt64GoWp6OP5C8/HY3dlznfbrn6ZvWBr\r\nVsnc7VZ49r8r/QvRKRrljkIQuNwaW/LmRxJ1AVGGiorspmrdz0Lf2WOXnLH/+4lR\r\nq5dar5YmGqi9Mo2j5ALg/2MiO1PonuKs1eX9tLZCCeanXFexU0qaeFZelunJ6UUm\r\nBXpaGO1QfECcNnvarudosbe1ve6YGABn8MpAY+8zdYnYHPB8Pojhzvk7/8PMHoPu\r\nnjDb3lZT+b8BDmNz+GISCUSHYkdK2rWh+8wqD3T5rOtzTqAPuSNeDNocUF6wzxem\r\nHWqvP7yM3VoXYvn7FA4NCa9mE3k8MzGCApcwggKTAgEBMIGUMHwxCzAJBgNVBAYT\r\nAlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNh\r\ndGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZh\r\nbGlkLWNhQGV4YW1wbGUuY29tAhQoC/xUcLhcK13sGSiYxuUPf758tDAJBgUrDgMC\r\nGgUAoIHYMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X\r\nDTIyMTEwNDEzMzE1NVowIwYJKoZIhvcNAQkEMRYEFK+sB97I9ZGpDGUMWLsE3dCU\r\n/pbtMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjAL\r\nBglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3\r\nDQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIB\r\nAFli04jnSd0d0lnE9GO23qzxI98QIbCz1NU1qk+zDyAhwOWQJdMUfuekk3g4Gn2q\r\nOFzdvlIMpgG2W7AVZlLUQMQjWIoDWkTeqxM8n6StoXcDvlArBHQDbourufFUu7OE\r\n3TVsI6l/jZG3Xub9Uar0S3lF6rQ/A3vl28poRL/EIQye6ypg6UbS/EvkvfbsKUJD\r\n6SpExlwh0R7lk1g/xn3tFVSEAH7VSJYr/8C/Bak06NPOtZZSSeU9ryRzeK/gN3SJ\r\nnrEp+NkTezzApjSnZasrPSbzmRL4+18x3kmAmnwR3aRi7F7KhCs78qPUEVP5XbhX\r\n2hO9RjMy6Uki+R/AG4aempk=\r\n\r\n------1FD9CF28CC0AD72EF1FF6D0511838F0E--\r\n\r\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/email/invalid-protected-mail-subject.eml",
    "content": "From: valid-ca@example.com\r\nTo: recipient@example.org\r\nSubject: ACME: aDiFfErEnTtOkEn\r\nAuto-Submitted: auto-generated; type=acme\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: multipart/signed; protocol=\"application/x-pkcs7-signature\"; micalg=\"sha1\"; boundary=\"----6E9953AAECB0BDB6F65BCD88900D3E15\"\r\n\r\nThis is an S/MIME signed message\r\n\r\n------6E9953AAECB0BDB6F65BCD88900D3E15\r\nContent-Type: message/RFC822; forwarded=no\r\n\r\nFrom: valid-ca@example.com\r\nTo: recipient@example.org\r\nSubject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nThis is an automatically generated ACME challenge.\r\n\r\n------6E9953AAECB0BDB6F65BCD88900D3E15\r\nContent-Type: application/x-pkcs7-signature; name=\"smime.p7s\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"smime.p7s\"\r\n\r\nMIIGzQYJKoZIhvcNAQcCoIIGvjCCBroCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3\r\nDQEHAaCCA/4wggP6MIIC4qADAgECAhQoC/xUcLhcK13sGSiYxuUPf758tDANBgkq\r\nhkiG9w0BAQsFADB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w\r\nHAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu\r\nY29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjEx\r\nMDQxMjU1MzlaFw0zMjExMDExMjU1MzlaMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQH\r\nDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIG\r\nA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1w\r\nbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzTyW1VRm+mQH\r\nYU5hZWgzgrPRkRzMLlLoCamlRs5DjGf3zIpo9a/17m60YfIXBJruImUBXxa5lp0Z\r\nqaayty+nZOpS0wBZSqTLuslZ0WuyyyW2DEBNO7jLW58cMn8MfAwMYjDcxtubNb7M\r\nAG9iZRj6wn6tKCsXtYUgAIpNhyPPtDuEZ5df1ecOnvlW2vO+MwytM8DLLtwolET/\r\ntPzOXPHDiyKjij02jyJ1DlZxptKudiKaBeY1WY/W/PpS6fGskTJQc/bZPgE3OP/9\r\nY9Y1uzZTulkO3R6MlLNdLam32/ehpJvSWyfSbToyC2ejvaXoRChPFcAmTpqA0HPg\r\nh8sudiS14QIDAQABo3QwcjAdBgNVHQ4EFgQUR0gRYoOCPJYqFiPGwB141VTCXuIw\r\nHwYDVR0jBBgwFoAUR0gRYoOCPJYqFiPGwB141VTCXuIwDwYDVR0TAQH/BAUwAwEB\r\n/zAfBgNVHREEGDAWgRR2YWxpZC1jYUBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsF\r\nAAOCAQEAJDQsNTWxVLNHBrVl6p6IetzeBt64GoWp6OP5C8/HY3dlznfbrn6ZvWBr\r\nVsnc7VZ49r8r/QvRKRrljkIQuNwaW/LmRxJ1AVGGiorspmrdz0Lf2WOXnLH/+4lR\r\nq5dar5YmGqi9Mo2j5ALg/2MiO1PonuKs1eX9tLZCCeanXFexU0qaeFZelunJ6UUm\r\nBXpaGO1QfECcNnvarudosbe1ve6YGABn8MpAY+8zdYnYHPB8Pojhzvk7/8PMHoPu\r\nnjDb3lZT+b8BDmNz+GISCUSHYkdK2rWh+8wqD3T5rOtzTqAPuSNeDNocUF6wzxem\r\nHWqvP7yM3VoXYvn7FA4NCa9mE3k8MzGCApcwggKTAgEBMIGUMHwxCzAJBgNVBAYT\r\nAlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNh\r\ndGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZh\r\nbGlkLWNhQGV4YW1wbGUuY29tAhQoC/xUcLhcK13sGSiYxuUPf758tDAJBgUrDgMC\r\nGgUAoIHYMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X\r\nDTIyMTEwNDEzMzE1NVowIwYJKoZIhvcNAQkEMRYEFK+sB97I9ZGpDGUMWLsE3dCU\r\n/pbtMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjAL\r\nBglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3\r\nDQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIB\r\nAFli04jnSd0d0lnE9GO23qzxI98QIbCz1NU1qk+zDyAhwOWQJdMUfuekk3g4Gn2q\r\nOFzdvlIMpgG2W7AVZlLUQMQjWIoDWkTeqxM8n6StoXcDvlArBHQDbourufFUu7OE\r\n3TVsI6l/jZG3Xub9Uar0S3lF6rQ/A3vl28poRL/EIQye6ypg6UbS/EvkvfbsKUJD\r\n6SpExlwh0R7lk1g/xn3tFVSEAH7VSJYr/8C/Bak06NPOtZZSSeU9ryRzeK/gN3SJ\r\nnrEp+NkTezzApjSnZasrPSbzmRL4+18x3kmAmnwR3aRi7F7KhCs78qPUEVP5XbhX\r\n2hO9RjMy6Uki+R/AG4aempk=\r\n\r\n------6E9953AAECB0BDB6F65BCD88900D3E15--\r\n\r\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/email/invalid-protected-mail-to.eml",
    "content": "From: valid-ca@example.com\r\nTo: tampered-recipient@example.com\r\nSubject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\r\nAuto-Submitted: auto-generated; type=acme\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: multipart/signed; protocol=\"application/x-pkcs7-signature\"; micalg=\"sha1\"; boundary=\"----2D5F3855936C8172B69EB7BC1C12A23A\"\r\n\r\nThis is an S/MIME signed message\r\n\r\n------2D5F3855936C8172B69EB7BC1C12A23A\r\nContent-Type: message/RFC822; forwarded=no\r\n\r\nFrom: valid-ca@example.com\r\nTo: recipient@example.org\r\nSubject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nThis is an automatically generated ACME challenge.\r\n\r\n------2D5F3855936C8172B69EB7BC1C12A23A\r\nContent-Type: application/x-pkcs7-signature; name=\"smime.p7s\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"smime.p7s\"\r\n\r\nMIIGzQYJKoZIhvcNAQcCoIIGvjCCBroCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3\r\nDQEHAaCCA/4wggP6MIIC4qADAgECAhQoC/xUcLhcK13sGSiYxuUPf758tDANBgkq\r\nhkiG9w0BAQsFADB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w\r\nHAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu\r\nY29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjEx\r\nMDQxMjU1MzlaFw0zMjExMDExMjU1MzlaMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQH\r\nDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIG\r\nA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1w\r\nbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzTyW1VRm+mQH\r\nYU5hZWgzgrPRkRzMLlLoCamlRs5DjGf3zIpo9a/17m60YfIXBJruImUBXxa5lp0Z\r\nqaayty+nZOpS0wBZSqTLuslZ0WuyyyW2DEBNO7jLW58cMn8MfAwMYjDcxtubNb7M\r\nAG9iZRj6wn6tKCsXtYUgAIpNhyPPtDuEZ5df1ecOnvlW2vO+MwytM8DLLtwolET/\r\ntPzOXPHDiyKjij02jyJ1DlZxptKudiKaBeY1WY/W/PpS6fGskTJQc/bZPgE3OP/9\r\nY9Y1uzZTulkO3R6MlLNdLam32/ehpJvSWyfSbToyC2ejvaXoRChPFcAmTpqA0HPg\r\nh8sudiS14QIDAQABo3QwcjAdBgNVHQ4EFgQUR0gRYoOCPJYqFiPGwB141VTCXuIw\r\nHwYDVR0jBBgwFoAUR0gRYoOCPJYqFiPGwB141VTCXuIwDwYDVR0TAQH/BAUwAwEB\r\n/zAfBgNVHREEGDAWgRR2YWxpZC1jYUBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsF\r\nAAOCAQEAJDQsNTWxVLNHBrVl6p6IetzeBt64GoWp6OP5C8/HY3dlznfbrn6ZvWBr\r\nVsnc7VZ49r8r/QvRKRrljkIQuNwaW/LmRxJ1AVGGiorspmrdz0Lf2WOXnLH/+4lR\r\nq5dar5YmGqi9Mo2j5ALg/2MiO1PonuKs1eX9tLZCCeanXFexU0qaeFZelunJ6UUm\r\nBXpaGO1QfECcNnvarudosbe1ve6YGABn8MpAY+8zdYnYHPB8Pojhzvk7/8PMHoPu\r\nnjDb3lZT+b8BDmNz+GISCUSHYkdK2rWh+8wqD3T5rOtzTqAPuSNeDNocUF6wzxem\r\nHWqvP7yM3VoXYvn7FA4NCa9mE3k8MzGCApcwggKTAgEBMIGUMHwxCzAJBgNVBAYT\r\nAlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNh\r\ndGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZh\r\nbGlkLWNhQGV4YW1wbGUuY29tAhQoC/xUcLhcK13sGSiYxuUPf758tDAJBgUrDgMC\r\nGgUAoIHYMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X\r\nDTIyMTEwNDEzMzE1NVowIwYJKoZIhvcNAQkEMRYEFK+sB97I9ZGpDGUMWLsE3dCU\r\n/pbtMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjAL\r\nBglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3\r\nDQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIB\r\nAFli04jnSd0d0lnE9GO23qzxI98QIbCz1NU1qk+zDyAhwOWQJdMUfuekk3g4Gn2q\r\nOFzdvlIMpgG2W7AVZlLUQMQjWIoDWkTeqxM8n6StoXcDvlArBHQDbourufFUu7OE\r\n3TVsI6l/jZG3Xub9Uar0S3lF6rQ/A3vl28poRL/EIQye6ypg6UbS/EvkvfbsKUJD\r\n6SpExlwh0R7lk1g/xn3tFVSEAH7VSJYr/8C/Bak06NPOtZZSSeU9ryRzeK/gN3SJ\r\nnrEp+NkTezzApjSnZasrPSbzmRL4+18x3kmAmnwR3aRi7F7KhCs78qPUEVP5XbhX\r\n2hO9RjMy6Uki+R/AG4aempk=\r\n\r\n------2D5F3855936C8172B69EB7BC1C12A23A--\r\n\r\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/email/invalid-signed-mail.eml",
    "content": "From: valid-ca@example.com\r\nTo: recipient@example.org\r\nSubject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\r\nAuto-Submitted: auto-generated; type=acme\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: multipart/signed; protocol=\"application/x-pkcs7-signature\"; micalg=\"sha1\"; boundary=\"----2D188458DC295B22904B7A1FB62F57BF\"\r\n\r\nThis is an S/MIME signed message\r\n\r\n------2D188458DC295B22904B7A1FB62F57BF\r\nContent-Type: message/RFC822; forwarded=no\r\n\r\nFrom: valid-ca@example.com\r\nTo: recipient@example.org\r\nSubject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nThis is an automatically generated ACME challenge.\r\n\r\n------2D188458DC295B22904B7A1FB62F57BF\r\nContent-Type: application/x-pkcs7-signature; name=\"smime.p7s\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"smime.p7s\"\r\n\r\nMIIG1QYJKoZIhvcNAQcCoIIGxjCCBsICAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3\r\nDQEHAaCCBAQwggQAMIIC6KADAgECAhRuqrQwjQAEKPLiumz639inbqPeJzANBgkq\r\nhkiG9w0BAQsFADB+MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w\r\nHAYDVQQKDBVFbWNhIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu\r\nY29tMSUwIwYJKoZIhvcNAQkBFhZpbnZhbGlkLWNhQGV4YW1wbGUuY29tMB4XDTIy\r\nMTEwNDEyNTUzOVoXDTMyMTEwMTEyNTUzOVowfjELMAkGA1UEBhMCWFgxEjAQBgNV\r\nBAcMCUFjbWUgQ2l0eTEeMBwGA1UECgwVRW1jYSBDZXJ0aWZpY2F0ZXMgTHRkMRQw\r\nEgYDVQQDDAtleGFtcGxlLmNvbTElMCMGCSqGSIb3DQEJARYWaW52YWxpZC1jYUBl\r\neGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMxVUSNR\r\npIreCKvtK148IWN7MWfK8fymmPOF8oQcFieCC0mfw8efc/4MEVA5qN3avHOXd1RG\r\nVgaR+tM30zRiTLllc6YnPePUPZNSQmJcnXgMlRhmOeCfo2hNglWFBnP/CV29xarP\r\nCf94DqXGrLZ8L8uGtk/JsNOreced34V4RZ9WvN53HlyiNtEJJLggM17wzJZcV+rQ\r\n7LtsBHZfDOdTScCpEDqLZDmLMVLEBUtrwo5+5mYw4M0PDEP2D4qPux7NAHuaG66F\r\nZt7mq6DcceG/AneuUN7xOyMQ9x/D3NfiSgXbZeJM+BbE0cT7EY9WdZBtsS6HjJA0\r\nyt98FgAIDoSzRQcCAwEAAaN2MHQwHQYDVR0OBBYEFB0Xb1ErzzEjzPrJC8PvkAJ3\r\nqnoGMB8GA1UdIwQYMBaAFB0Xb1ErzzEjzPrJC8PvkAJ3qnoGMA8GA1UdEwEB/wQF\r\nMAMBAf8wIQYDVR0RBBowGIEWaW52YWxpZC1jYUBleGFtcGxlLmNvbTANBgkqhkiG\r\n9w0BAQsFAAOCAQEAnwFh8H7lwMWrHSceM6MVt+5M0yVX/r6K5YWGI/AaFG2Q5jkz\r\n6yIeESgiXukza4oKY1I1clZwWus9fnrwn+AWbtvKbGLklFWCUB60fx82dZwoO14Q\r\nTm3GX6wwC0Y5eFYiXwEJ4gnazBWEWscp4E94AKqr1EYuOI9sR22l/rNtANrEiVsT\r\nP4+kUgLEr9Y5infYglXQMjDVfNXRSETBnx4a5Fljd7pSD4e7H19eMiByd78q98ze\r\nt0g2anl2cJbdM6cgu5iyAgS3BgMrFMnd8m7KkZwum+tslNWA1tbDGK0AWH7ztjh3\r\norifkxq2Hw6tdypZoWoLrwSEDEvNIy8+sQjUOTGCApkwggKVAgEBMIGWMH4xCzAJ\r\nBgNVBAYTAlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoMFUVtY2EgQ2Vy\r\ndGlmaWNhdGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xJTAjBgkqhkiG9w0B\r\nCQEWFmludmFsaWQtY2FAZXhhbXBsZS5jb20CFG6qtDCNAAQo8uK6bPrf2Kduo94n\r\nMAkGBSsOAwIaBQCggdgwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG\r\n9w0BCQUxDxcNMjIxMTA0MTMzMTU1WjAjBgkqhkiG9w0BCQQxFgQUr6wH3sj1kakM\r\nZQxYuwTd0JT+lu0weQYJKoZIhvcNAQkPMWwwajALBglghkgBZQMEASowCwYJYIZI\r\nAWUDBAEWMAsGCWCGSAFlAwQBAjAKBggqhkiG9w0DBzAOBggqhkiG9w0DAgICAIAw\r\nDQYIKoZIhvcNAwICAUAwBwYFKw4DAgcwDQYIKoZIhvcNAwICASgwDQYJKoZIhvcN\r\nAQEBBQAEggEAdZ4XoWViOrqOd4kWZVD+12GXJ7NcCWHOiTdXZ0S/3TKwwTPwQ0f0\r\nXZmk8iJwcfXqAv48ITK+yh4jk8urtrjS3xohHGxVonifbgxoLm/yHqA13D4F0M9q\r\nr0jfmXAWfH1HDk6AtZA5c1IJVJNMcVcLHkib232FgpicgPEZNWvRr8zHCNN3dymF\r\nQme9h2BqHxy2+nX96BBEiRMImG9Z9G4+sOqpwiDTNgzr7nFtldKtjiV/GarBeteZ\r\nnx4QcHXY307ydLDrh2JzLK/+LZEhTMBQ2rWGfAo/owDGi/Pal//SgqQWFy3luJp2\r\n8WG34Z4CnG514YmRzN2Z1V3fvreRInllkQ==\r\n\r\n------2D188458DC295B22904B7A1FB62F57BF--\r\n\r\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/email/valid-mail-7508.eml",
    "content": "Return-Path: <acme-challenge@dc-bsd.my.corp>\r\nX-Original-To: gitlab@dc-bsd.my.corp\r\nDelivered-To: gitlab@dc-bsd.my.corp\r\nReceived: from [127.0.0.1] (acme-ca-02.dc-bsd.my.corp [10.70.15.231])\r\n\tby mail.dc-bsd.my.corp (Postfix) with ESMTP id 0119D9CC22\r\n\tfor <gitlab@dc-bsd.my.corp>; Sat, 26 Nov 2022 21:09:39 +0000 (UTC)\r\nContent-Type: multipart/signed; protocol=\"application/pkcs7-signature\";\r\n micalg=sha256; boundary=\"--_NmP-1d902f4d1e8a735a-Part_1\"\r\nAuto-Submitted: auto-generated; type=acme\r\nFrom: acme-challenge@dc-bsd.my.corp\r\nTo: gitlab@dc-bsd.my.corp\r\nSubject: ACME: ABxfL5s4bjvmyVRvl6y-Y_GhdzTdWpKqlmrKAIVe\r\nMessage-ID: <ee3f3a6d-12c4-7cc0-aff2-b5a9b19a9f7e@dc-bsd.my.corp>\r\nDate: Sat, 26 Nov 2022 21:09:38 +0000\r\nMIME-Version: 1.0\r\n\r\n----_NmP-1d902f4d1e8a735a-Part_1\r\nContent-Type: multipart/alternative;\r\n boundary=\"--_NmP-1d902f4d1e8a735a-Part_2\"\r\n\r\n----_NmP-1d902f4d1e8a735a-Part_2\r\nContent-Type: text/html; charset=utf-8\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n<html><p>This is an automatically generated ACME challenge for email =\r\naddress <em>gitlab@dc-bsd.my.corp</em>. If you haven't requested an S/MIME =\r\ncertificate generation for this email address, be very afraid. If you did =\r\nrequest it, your email client might be able to process this request =\r\nautomatically, or you might have to paste the first token part into an =\r\nexternal program.</p> <p>Please reply to this mail and fill out the =\r\nfollowing template: <pre>-----BEGIN ACME RESPONSE-----\r\n&lt;fill in challengeResponse here&gt;\r\n-----END ACME RESPONSE-----\r\n</pre>Use the value of the following calculation inside the ACME response:\r\n<pre>  token =3D (decodeBase64url(token-part1) || decodeBase64url(token-par=\r\nt2))\r\n  keyAuthorization =3D base64url(token) || '.' || base64url(Thumbprint(acco=\r\nuntKey))\r\n  challengeResponse =3D base64url(SHA256(keyAuthorization))\r\n</pre>Where can I find all the ingredients for this?<ul><li>token-part1 is =\r\nin the subject of this email after 'ACME: ',</li><li>token-part2 can be =\r\nfound in your challenge request (over https),</li><li>accountKey has been =\r\ngenerated in your ACME client.</li></ul></p></html>\r\n----_NmP-1d902f4d1e8a735a-Part_2\r\nContent-Type: application/json; charset=utf8\r\nContent-Encoding: utf8\r\n\r\n{ \"token-part1\": \"001c5f2f9b386e3be6c9546f97acbe63f1a17734dd5a92aa966aca00855e\" }\r\n----_NmP-1d902f4d1e8a735a-Part_2--\r\n\r\n----_NmP-1d902f4d1e8a735a-Part_1\r\nContent-Type: application/pkcs7-signature; name=smime.p7s\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=smime.p7s\r\n\r\nMIIUXQYJKoZIhvcNAQcCoIIUTjCCFEoCAQExDzANBglghkgBZQMEAgEFADCCBkkGCSqGSIb3DQEH\r\nAaCCBjoEggY2Q29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7DQogYm91bmRhcnk9\r\nIi0tX05tUC0xZDkwMmY0ZDFlOGE3MzVhLVBhcnRfMiINCg0KLS0tLV9ObVAtMWQ5MDJmNGQxZThh\r\nNzM1YS1QYXJ0XzINCkNvbnRlbnQtVHlwZTogdGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04DQpDb250\r\nZW50LVRyYW5zZmVyLUVuY29kaW5nOiBxdW90ZWQtcHJpbnRhYmxlDQoNCjxodG1sPjxwPlRoaXMg\r\naXMgYW4gYXV0b21hdGljYWxseSBnZW5lcmF0ZWQgQUNNRSBjaGFsbGVuZ2UgZm9yIGVtYWlsID0N\r\nCmFkZHJlc3MgPGVtPmdpdGxhYkBkYy1ic2QubXkuY29ycDwvZW0+LiBJZiB5b3UgaGF2ZW4ndCBy\r\nZXF1ZXN0ZWQgYW4gUy9NSU1FID0NCmNlcnRpZmljYXRlIGdlbmVyYXRpb24gZm9yIHRoaXMgZW1h\r\naWwgYWRkcmVzcywgYmUgdmVyeSBhZnJhaWQuIElmIHlvdSBkaWQgPQ0KcmVxdWVzdCBpdCwgeW91\r\nciBlbWFpbCBjbGllbnQgbWlnaHQgYmUgYWJsZSB0byBwcm9jZXNzIHRoaXMgcmVxdWVzdCA9DQph\r\ndXRvbWF0aWNhbGx5LCBvciB5b3UgbWlnaHQgaGF2ZSB0byBwYXN0ZSB0aGUgZmlyc3QgdG9rZW4g\r\ncGFydCBpbnRvIGFuID0NCmV4dGVybmFsIHByb2dyYW0uPC9wPiA8cD5QbGVhc2UgcmVwbHkgdG8g\r\ndGhpcyBtYWlsIGFuZCBmaWxsIG91dCB0aGUgPQ0KZm9sbG93aW5nIHRlbXBsYXRlOiA8cHJlPi0t\r\nLS0tQkVHSU4gQUNNRSBSRVNQT05TRS0tLS0tDQombHQ7ZmlsbCBpbiBjaGFsbGVuZ2VSZXNwb25z\r\nZSBoZXJlJmd0Ow0KLS0tLS1FTkQgQUNNRSBSRVNQT05TRS0tLS0tDQo8L3ByZT5Vc2UgdGhlIHZh\r\nbHVlIG9mIHRoZSBmb2xsb3dpbmcgY2FsY3VsYXRpb24gaW5zaWRlIHRoZSBBQ01FIHJlc3BvbnNl\r\nOg0KPHByZT4gIHRva2VuID0zRCAoZGVjb2RlQmFzZTY0dXJsKHRva2VuLXBhcnQxKSB8fCBkZWNv\r\nZGVCYXNlNjR1cmwodG9rZW4tcGFyPQ0KdDIpKQ0KICBrZXlBdXRob3JpemF0aW9uID0zRCBiYXNl\r\nNjR1cmwodG9rZW4pIHx8ICcuJyB8fCBiYXNlNjR1cmwoVGh1bWJwcmludChhY2NvPQ0KdW50S2V5\r\nKSkNCiAgY2hhbGxlbmdlUmVzcG9uc2UgPTNEIGJhc2U2NHVybChTSEEyNTYoa2V5QXV0aG9yaXph\r\ndGlvbikpDQo8L3ByZT5XaGVyZSBjYW4gSSBmaW5kIGFsbCB0aGUgaW5ncmVkaWVudHMgZm9yIHRo\r\naXM/PHVsPjxsaT50b2tlbi1wYXJ0MSBpcyA9DQppbiB0aGUgc3ViamVjdCBvZiB0aGlzIGVtYWls\r\nIGFmdGVyICdBQ01FOiAnLDwvbGk+PGxpPnRva2VuLXBhcnQyIGNhbiBiZSA9DQpmb3VuZCBpbiB5\r\nb3VyIGNoYWxsZW5nZSByZXF1ZXN0IChvdmVyIGh0dHBzKSw8L2xpPjxsaT5hY2NvdW50S2V5IGhh\r\ncyBiZWVuID0NCmdlbmVyYXRlZCBpbiB5b3VyIEFDTUUgY2xpZW50LjwvbGk+PC91bD48L3A+PC9o\r\ndG1sPg0KLS0tLV9ObVAtMWQ5MDJmNGQxZThhNzM1YS1QYXJ0XzINCkNvbnRlbnQtVHlwZTogYXBw\r\nbGljYXRpb24vanNvbjsgY2hhcnNldD11dGY4DQpDb250ZW50LUVuY29kaW5nOiB1dGY4DQoNCnsg\r\nInRva2VuLXBhcnQxIjogIjAwMWM1ZjJmOWIzODZlM2JlNmM5NTQ2Zjk3YWNiZTYzZjFhMTc3MzRk\r\nZDVhOTJhYTk2NmFjYTAwODU1ZSIgfQ0KLS0tLV9ObVAtMWQ5MDJmNGQxZThhNzM1YS1QYXJ0XzIt\r\nLQ0KoIIK5DCCBI8wggQUoAMCAQICFGFq2BiR4ebW84K3/HrjMpkODKUtMAoGCCqGSM49BAMDMEkx\r\nCzAJBgNVBAYTAkpQMQ4wDAYDVQQHDAVUb2tpbzEQMA4GA1UECgwHTXkuQ29ycDEYMBYGA1UEAwwP\r\nUm9vdCBDQSAyMDIyIEcxMB4XDTIyMTEyNDIxNDAxN1oXDTI1MDUyODIxNDAxNlowSDELMAkGA1UE\r\nBhMCSlAxDjAMBgNVBAcMBVRva2lvMRAwDgYDVQQKDAdNeS5Db3JwMRcwFQYDVQQDDA5TdWIgQ0Eg\r\nMjAyMiBHMTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAIILhCsJsrzhrAELwxAbrKaH\r\nALeP7liQqUiPDbN+JfRvqyEBpBG8JBkGC76uM5TIQ+f+28IBIs0YwdjIiCNPeew/nAPZ1aNZEYco\r\nvQ7G3CQ7mUAhFkeGKOVaS+2LOqvmdry5p1SKlC0ziw/Yf3dD1KSQ4sBPBa7gOp8pDpe9YKX/j/fX\r\nk83iSTFvrp1LTe4BILMh9YzVAukKR5A3WgVkrR156gLoV6Rew6uSwHAOtl4JAYBclRohmvbjU/FP\r\nk2kX8h6NK+V21HXD0inhyI/NnyVPxEO+n2isnrrtz/R6t2pUPn28xhNjbnFEBk8KZ4fQH23/BRtA\r\ncIibh3dzetVRJdoVZEjoYmZdveMB6TqsJJtlSSHTVIKcWNUFKAiqS5wSHVaX2XnjdVgqkQU3j5kD\r\nJBCwPG47KpoNdTTa4JWQwI9G57kGAvPomj+6ljYSavzfCeQ8nxwkoRhG0eXjalpOnjUPAxowqhmE\r\nmRYTg7hzIFPXYInhFIvHwd4zFkPh8HOa7wIDAQABo4IBjjCCAYowEgYDVR0TAQH/BAgwBgEB/wIB\r\nADAOBgNVHQ8BAf8EBAMCAYYwHgYDVR0gBBcwFTAIBgZngQwBAgEwCQYHZ4EMAQUBAjB+BggrBgEF\r\nBQcBAQRyMHAwbgYIKwYBBQUHMAKGYmh0dHA6Ly9hY21lLWNhLTAxLmRjLWJzZC5teS5jb3JwL2Rv\r\nd25sb2FkL1Jvb3QvNTQ1MWYxOWZkZDg5MmNjYmU5YmU3ODM1ODc2NjcwZmQxYjY5OTllOC9jYS5j\r\ncnQuY2VyMGwGA1UdHwRlMGMwYaBfoF2GW2h0dHA6Ly9hY21lLWNhLTAxLmRjLWJzZC5teS5jb3Jw\r\nL2Rvd25sb2FkL1Jvb3QvNTQ1MWYxOWZkZDg5MmNjYmU5YmU3ODM1ODc2NjcwZmQxYjY5OTllOC5j\r\ncmwwKwYDVR0jBCQwIoAgxGqTGFlSJtP48rVLeyMc58TIb5WWC/mBW4AKc0OmIm0wKQYDVR0OBCIE\r\nINUBWh3giyFeu6hqGeExhD+S2sWYkeQcsGY6tX6AUgULMAoGCCqGSM49BAMDA2kAMGYCMQDW0NKq\r\nk5JJl+DQvDBAVWZ99LFzKP2y9H8RHNynK2g+VlF6h2141+SWO+ev2GAFj5kCMQC8HuhmB1g+aYuP\r\nwcuBtpOSZGrdTHXFmRubI/rb4K8rZTgP/FQXzbK81Mctv1V+AxgwggZNMIIEtaADAgECAhRTWC34\r\n33N0r2EkwdKjLxrvITOOgDANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJKUDEOMAwGA1UEBwwF\r\nVG9raW8xEDAOBgNVBAoMB015LkNvcnAxFzAVBgNVBAMMDlN1YiBDQSAyMDIyIEcxMB4XDTIyMTEy\r\nNDIxNDExNVoXDTIzMDIyMjIxNDExNFowVzELMAkGA1UEBhMCSlAxDjAMBgNVBAcMBVRva2lvMRAw\r\nDgYDVQQKDAdNeS5Db3JwMSYwJAYDVQQDFh1hY21lLWNoYWxsZW5nZUBkYy1ic2QubXkuY29ycDCC\r\nAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAInbq8HuIIHbaSgUP5jMSZuIt82r34Cbd4b6\r\nC1OgzJWW2CY++t1wkwmAA4NRHZEesm3c6AsqfYbbvifdAlll4bDuQXAtMDphyICgllLqyvdhI7ya\r\nFjhMzTyGHKXKztbjNyLwz9gFP/g6v/44EsPdagaFaVjFGeaBGr/JSih2cdBo8oICaEFhRymtARQP\r\n95mu+A0bV16zE2JnSqvN6ivIrN2uHQjlo0uymWRXJsK3Jv3Xm71vnKiESclkYQHNXi7PyA1GuOEO\r\noxSln7FQhzXYYurGenjzg9WAemtPPVw3jcMg9rM1k+Lkns/VLj3VTXhDZg1Ye6CsmTpUEmVo3seu\r\nvM2Y8GyTndmNt/tVAwiD77Ok5BRbSCBzQkopwMuzajg5PYXd0VGjhMUOxfgZavZGGFpjwPkEqY7s\r\nhhSwp5Wmq5OBehpPmh47ILHgUrctjALcgWgc4gaLwTVeDOOvHA0IbQBAFqD1mxNb1i4gSEJc9dUa\r\nlxc4LGa6ubhVLsNg9n7mdQIDAQABo4ICHjCCAhowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBaAw\r\nFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwQwKAYDVR0RBCEwH4EdYWNtZS1jaGFsbGVuZ2VAZGMtYnNk\r\nLm15LmNvcnAwEwYDVR0gBAwwCjAIBgZngQwBAgIwgeAGCCsGAQUFBwEBBIHTMIHQMF8GCCsGAQUF\r\nBzABhlNodHRwOi8vYWNtZS1jYS0wMS5kYy1ic2QubXkuY29ycC9vY3NwL1N1Yi82MTZhZDgxODkx\r\nZTFlNmQ2ZjM4MmI3ZmM3YWUzMzI5OTBlMGNhNTJkLzBtBggrBgEFBQcwAoZhaHR0cDovL2FjbWUt\r\nY2EtMDEuZGMtYnNkLm15LmNvcnAvZG93bmxvYWQvU3ViLzYxNmFkODE4OTFlMWU2ZDZmMzgyYjdm\r\nYzdhZTMzMjk5MGUwY2E1MmQvY2EuY3J0LmNlcjBrBgNVHR8EZDBiMGCgXqBchlpodHRwOi8vYWNt\r\nZS1jYS0wMS5kYy1ic2QubXkuY29ycC9kb3dubG9hZC9TdWIvNjE2YWQ4MTg5MWUxZTZkNmYzODJi\r\nN2ZjN2FlMzMyOTkwZTBjYTUyZC5jcmwwKwYDVR0jBCQwIoAg1QFaHeCLIV67qGoZ4TGEP5LaxZiR\r\n5BywZjq1foBSBQswKQYDVR0OBCIEIFWVbzWpC2IKkBu6DBLEtmO/1GjEXKJaYigKZkkPYBsgMA0G\r\nCSqGSIb3DQEBCwUAA4IBgQAFvMhQ3dpIYwXch5Op0v3qhnRpZFpZh/3DCjLAQt2d+O71nVGGt6Bj\r\njsiewHU6Dtl5zxDZfz9cQTaRhEWfXka2byFWKfYw3xq64cmr5a7+9RynvtrSBjx9a+IOVNAylyjK\r\nxnNdWVZLa0Qna1Qqngz32i7aXxENuES2kZcBTnLDEaA3WBuGdlqBsFn1T0A6pP20/np8TS4Q17hR\r\nGEAE9jrhcmRcbP7ABqDz7+Jcq0qnpXE8zJwm9DsR4HJ1Cudsm3iIz3BSDOchUMPPcOOG+JltYOus\r\neaheeNFDdyxKIx4TZkRwI+avl27DJ4O2n/OKqDQZTYcg/HxH2xWwNfq2/Z08YeH/I4xJnlPxZSDG\r\nDjvVNcQMsAwOdG2O55Lq7q4wmp7ZH2JVA97vekBAJwltjyy3APDOeqi4CnzlaZcOn0GRg7MasQmF\r\nAYbOlo8Ti8oe4U65w8Y2q4ip1EbiwfNMrAkcaAVGIJm2pTapp9nOraed68uMogF7xPhucJLA7Zer\r\nWMMxggL9MIIC+QIBATBgMEgxCzAJBgNVBAYTAkpQMQ4wDAYDVQQHDAVUb2tpbzEQMA4GA1UECgwH\r\nTXkuQ29ycDEXMBUGA1UEAwwOU3ViIENBIDIwMjIgRzECFFNYLfjfc3SvYSTB0qMvGu8hM46AMA0G\r\nCWCGSAFlAwQCAQUAoIHvMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwLwYJKoZIhvcNAQkEMSIE\r\nIIbuwptDlJ0DVnN9vhoyQl6l6BeOjhuVjyRkfKXrS2bSMIGhBgsqhkiG9w0BCRACNzGBkTGBjgoB\r\nATCBiDA8GgdTdWJqZWN0DC5BQ01FOiBBQnhmTDVzNGJqdm15VlJ2bDZ5LVlfR2hkelRkV3BLcWxt\r\ncktBSVZlAgEAMB4aAlRvDBVnaXRsYWJAZGMtYnNkLm15LmNvcnACAQAwKBoERnJvbQwdYWNtZS1j\r\naGFsbGVuZ2VAZGMtYnNkLm15LmNvcnACAQAwDQYJKoZIhvcNAQELBQAEggGAGUyk3DnAEFJeB3xS\r\nqbgx6NdbkP3G1ah9el01wEez4jQC1pvRe07dMApkJImlSxnOB5Lp/06a14YvAu/rGdmTIAZjeuXV\r\nVPxo+urSTUj4dZB26HNKHyUwXvKWgIhfwlrc/kK+sXyYXk8cNNtAuOQlTJyU1B1NG5nFKTi4UOZI\r\n9B1Lc0MktnrAntB70bOVcT0Lz5Qgslc8jwIUePJLyGKohKTe/744QPqZAz7nQaXS9E30yd1oTB7I\r\nFH4Fg9GjWnD8rLvdCR/S54zEqsdIX/YCmky5DzyjLthY1Al4AJwaodd5gPQn2xhtkxssHLg48/X+\r\n3Se+JTyad6MXmMixnlJFL8DoL8r/BcfZbT4b8oLWMrbURooMPVnxYbTx67IOGxuIYFolM9d9F39P\r\nGBcFkTGIbcVUOIyV2vGH0fXOiVx4ktm4j3Ds843KZ8hj52SZr7RBAsgileucs713UOjfhrQqkbZh\r\nnF5r9fOKs8ky8tSePQ8izk/jXC6PY5D7xyELoPrh\r\n----_NmP-1d902f4d1e8a735a-Part_1--"
  },
  {
    "path": "acme4j-smime/src/test/resources/email/valid-mail.eml",
    "content": "From: valid-ca@example.com\r\nTo: recipient@example.org\r\nSubject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\r\nAuto-Submitted: auto-generated; type=acme\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: multipart/signed; protocol=\"application/x-pkcs7-signature\"; micalg=\"sha1\"; boundary=\"----163CF1BA3ECF9F288779BFBE9EF3E10C\"\r\n\r\nThis is an S/MIME signed message\r\n\r\n------163CF1BA3ECF9F288779BFBE9EF3E10C\r\nContent-Type: message/RFC822; forwarded=no\r\n\r\nFrom: valid-ca@example.com\r\nTo: recipient@example.org\r\nSubject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=\r\nMessage-ID: <A2299BB.FF7788@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nThis is an automatically generated ACME challenge.\r\n\r\n------163CF1BA3ECF9F288779BFBE9EF3E10C\r\nContent-Type: application/x-pkcs7-signature; name=\"smime.p7s\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"smime.p7s\"\r\n\r\nMIIGzQYJKoZIhvcNAQcCoIIGvjCCBroCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3\r\nDQEHAaCCA/4wggP6MIIC4qADAgECAhQoC/xUcLhcK13sGSiYxuUPf758tDANBgkq\r\nhkiG9w0BAQsFADB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w\r\nHAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu\r\nY29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjEx\r\nMDQxMjU1MzlaFw0zMjExMDExMjU1MzlaMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQH\r\nDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIG\r\nA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1w\r\nbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzTyW1VRm+mQH\r\nYU5hZWgzgrPRkRzMLlLoCamlRs5DjGf3zIpo9a/17m60YfIXBJruImUBXxa5lp0Z\r\nqaayty+nZOpS0wBZSqTLuslZ0WuyyyW2DEBNO7jLW58cMn8MfAwMYjDcxtubNb7M\r\nAG9iZRj6wn6tKCsXtYUgAIpNhyPPtDuEZ5df1ecOnvlW2vO+MwytM8DLLtwolET/\r\ntPzOXPHDiyKjij02jyJ1DlZxptKudiKaBeY1WY/W/PpS6fGskTJQc/bZPgE3OP/9\r\nY9Y1uzZTulkO3R6MlLNdLam32/ehpJvSWyfSbToyC2ejvaXoRChPFcAmTpqA0HPg\r\nh8sudiS14QIDAQABo3QwcjAdBgNVHQ4EFgQUR0gRYoOCPJYqFiPGwB141VTCXuIw\r\nHwYDVR0jBBgwFoAUR0gRYoOCPJYqFiPGwB141VTCXuIwDwYDVR0TAQH/BAUwAwEB\r\n/zAfBgNVHREEGDAWgRR2YWxpZC1jYUBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsF\r\nAAOCAQEAJDQsNTWxVLNHBrVl6p6IetzeBt64GoWp6OP5C8/HY3dlznfbrn6ZvWBr\r\nVsnc7VZ49r8r/QvRKRrljkIQuNwaW/LmRxJ1AVGGiorspmrdz0Lf2WOXnLH/+4lR\r\nq5dar5YmGqi9Mo2j5ALg/2MiO1PonuKs1eX9tLZCCeanXFexU0qaeFZelunJ6UUm\r\nBXpaGO1QfECcNnvarudosbe1ve6YGABn8MpAY+8zdYnYHPB8Pojhzvk7/8PMHoPu\r\nnjDb3lZT+b8BDmNz+GISCUSHYkdK2rWh+8wqD3T5rOtzTqAPuSNeDNocUF6wzxem\r\nHWqvP7yM3VoXYvn7FA4NCa9mE3k8MzGCApcwggKTAgEBMIGUMHwxCzAJBgNVBAYT\r\nAlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNh\r\ndGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZh\r\nbGlkLWNhQGV4YW1wbGUuY29tAhQoC/xUcLhcK13sGSiYxuUPf758tDAJBgUrDgMC\r\nGgUAoIHYMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X\r\nDTIyMTEwNDEzMzE1NVowIwYJKoZIhvcNAQkEMRYEFK+sB97I9ZGpDGUMWLsE3dCU\r\n/pbtMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjAL\r\nBglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3\r\nDQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIB\r\nAFli04jnSd0d0lnE9GO23qzxI98QIbCz1NU1qk+zDyAhwOWQJdMUfuekk3g4Gn2q\r\nOFzdvlIMpgG2W7AVZlLUQMQjWIoDWkTeqxM8n6StoXcDvlArBHQDbourufFUu7OE\r\n3TVsI6l/jZG3Xub9Uar0S3lF6rQ/A3vl28poRL/EIQye6ypg6UbS/EvkvfbsKUJD\r\n6SpExlwh0R7lk1g/xn3tFVSEAH7VSJYr/8C/Bak06NPOtZZSSeU9ryRzeK/gN3SJ\r\nnrEp+NkTezzApjSnZasrPSbzmRL4+18x3kmAmnwR3aRi7F7KhCs78qPUEVP5XbhX\r\n2hO9RjMy6Uki+R/AG4aempk=\r\n\r\n------163CF1BA3ECF9F288779BFBE9EF3E10C--\r\n\r\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/invalid-signer-privkey.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMVVEjUaSK3gir\n7StePCFjezFnyvH8ppjzhfKEHBYnggtJn8PHn3P+DBFQOajd2rxzl3dURlYGkfrT\nN9M0Yky5ZXOmJz3j1D2TUkJiXJ14DJUYZjngn6NoTYJVhQZz/wldvcWqzwn/eA6l\nxqy2fC/LhrZPybDTq3nHnd+FeEWfVrzedx5cojbRCSS4IDNe8MyWXFfq0Oy7bAR2\nXwznU0nAqRA6i2Q5izFSxAVLa8KOfuZmMODNDwxD9g+Kj7sezQB7mhuuhWbe5qug\n3HHhvwJ3rlDe8TsjEPcfw9zX4koF22XiTPgWxNHE+xGPVnWQbbEuh4yQNMrffBYA\nCA6Es0UHAgMBAAECggEAByliW6OL6dYYZbY9U+M1pF/3/lRNoPZR3A8wzdKSMDZN\noPn5ibCcByZzIOW0dnopKr//TbPdZgON0ANf4rEjUUguAn/Tmn2g3t3+N6ZZWpDO\nVPmYQ7g0qP42eDreXAhvUprJJ9Bz4EFb+hF5kjfOEQsarrc5/GFBNm7hG7N4dTos\nANLHDVt9p++40mBC9UnFTI9fghYqqsBipI2hMcVtLlfsTyhfdYk2atC0YwWH2g7H\nnyFsfrgwNlza5iAKuQn1vGhM6nigGBiDfHifNoTHZPhIcgHRwPvZWCpxcAt8wPw1\n4blqZPRqmbOYQHisXVSbKJbgN8zZiZB2j/SSDv9IyQKBgQDqpiSaiBZ+bUfy5M07\nmSClnHR8/zulR7NuSWZ5C92w2EFYI/OkSwKeshKdmafCz9VeVkeCUjjRkAGOF2pr\nI1icdY0XDt/Pd+jQzDqTtiUwwMOLRyvV437nCsMer++LbKRblEL+uSLCPhV2dnyx\nGUXj9JJhnGeZ+xKH20cE1dr2NQKBgQDe7QG6UGrXUzfN2BttvJfOAKETtVfl1Mm0\nuEC2skPlY1KVFYOCMuOFXLHwlc0KYNzVh5qBH+3dPyX1zjTurXmnaOcW0LxgJ4JN\nvKiXDLrt8HYJqANjYCh7vaZmuZ8RduR4iS+E+JG6JLAdfGIzPvzzUgbo5hXuFnQE\ndN8gsJcFywKBgAF24fmY6dMGKZHJfcJmdT6zWELDcQLaDLOef6Y3vb1xzA6ZwtZ+\npViKMfWL1PExTNqW3UFh8/rS1D+nw8FBajcnwKapMBpiXDCZZbAwTdEdEttWqV5f\nWhZlCcyyOmN7XRc5OKXQT/g4XPftS1/rkXUXvKYhTMA4QehZJPtRvlkVAoGBAMS+\nh/fXYXQIjget4wdGmvPEumSad6jv09UbiIG1cxbQQeIxyo7uOr9IwAKFMyElu8D4\nnPO5KkVJpkb6Ztz/XY7SlqEcOCTkuavCBUjKg2/b+VEsZ1EdXJ1ZE7M1v526QInh\nCX9hobuXBZgAXuq7fKOCkXabGl+2kU4dl49SSvdhAoGAH06W9bZUiqkLIzv+LizW\nrVP9fN4A8EGNtdWVrVD0Ql0hh/PBqGiiVwg47LAVuvpIc9kDDQM/5A6tF7aOGRDR\nk5ZDIrnWmrGQcEMTcisc5OwPnNjLVzV9r0swmeWcqZDrfgxeKpG7vdRaQdR++0FU\n1zaGc8HFGtTeJw6f6ZYttg8=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/invalid-signer.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEADCCAuigAwIBAgIUbqq0MI0ABCjy4rps+t/Yp26j3icwDQYJKoZIhvcNAQEL\nBQAwfjELMAkGA1UEBhMCWFgxEjAQBgNVBAcMCUFjbWUgQ2l0eTEeMBwGA1UECgwV\nRW1jYSBDZXJ0aWZpY2F0ZXMgTHRkMRQwEgYDVQQDDAtleGFtcGxlLmNvbTElMCMG\nCSqGSIb3DQEJARYWaW52YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjExMDQxMjU1\nMzlaFw0zMjExMDExMjU1MzlaMH4xCzAJBgNVBAYTAlhYMRIwEAYDVQQHDAlBY21l\nIENpdHkxHjAcBgNVBAoMFUVtY2EgQ2VydGlmaWNhdGVzIEx0ZDEUMBIGA1UEAwwL\nZXhhbXBsZS5jb20xJTAjBgkqhkiG9w0BCQEWFmludmFsaWQtY2FAZXhhbXBsZS5j\nb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDMVVEjUaSK3gir7Ste\nPCFjezFnyvH8ppjzhfKEHBYnggtJn8PHn3P+DBFQOajd2rxzl3dURlYGkfrTN9M0\nYky5ZXOmJz3j1D2TUkJiXJ14DJUYZjngn6NoTYJVhQZz/wldvcWqzwn/eA6lxqy2\nfC/LhrZPybDTq3nHnd+FeEWfVrzedx5cojbRCSS4IDNe8MyWXFfq0Oy7bAR2Xwzn\nU0nAqRA6i2Q5izFSxAVLa8KOfuZmMODNDwxD9g+Kj7sezQB7mhuuhWbe5qug3HHh\nvwJ3rlDe8TsjEPcfw9zX4koF22XiTPgWxNHE+xGPVnWQbbEuh4yQNMrffBYACA6E\ns0UHAgMBAAGjdjB0MB0GA1UdDgQWBBQdF29RK88xI8z6yQvD75ACd6p6BjAfBgNV\nHSMEGDAWgBQdF29RK88xI8z6yQvD75ACd6p6BjAPBgNVHRMBAf8EBTADAQH/MCEG\nA1UdEQQaMBiBFmludmFsaWQtY2FAZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQAD\nggEBAJ8BYfB+5cDFqx0nHjOjFbfuTNMlV/6+iuWFhiPwGhRtkOY5M+siHhEoIl7p\nM2uKCmNSNXJWcFrrPX568J/gFm7bymxi5JRVglAetH8fNnWcKDteEE5txl+sMAtG\nOXhWIl8BCeIJ2swVhFrHKeBPeACqq9RGLjiPbEdtpf6zbQDaxIlbEz+PpFICxK/W\nOYp32IJV0DIw1XzV0UhEwZ8eGuRZY3e6Ug+Hux9fXjIgcne/KvfM3rdINmp5dnCW\n3TOnILuYsgIEtwYDKxTJ3fJuypGcLpvrbJTVgNbWwxitAFh+87Y4d6K4n5Math8O\nrXcqWaFqC68EhAxLzSMvPrEI1Dk=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/json/emailReplyChallenge.json",
    "content": "{\n  \"type\": \"email-reply-00\",\n  \"url\": \"https://example.com/acme/chall/ABprV_B7yEyA4f\",\n  \"status\": \"pending\",\n  \"from\": \"acme-generator@example.org\",\n  \"token\": \"DGyRejmCefe7v4NfDGDKfA\"\n}"
  },
  {
    "path": "acme4j-smime/src/test/resources/json/emailReplyChallengeMismatch.json",
    "content": "{\n  \"type\": \"email-reply-00\",\n  \"url\": \"https://example.com/acme/chall/CoFefB832SAebe\",\n  \"status\": \"pending\",\n  \"from\": \"acme-challenge+2i211oi1204310@example.com\",\n  \"token\": \"IAi2dsjFIDSJsifdj394wf\"\n}"
  },
  {
    "path": "acme4j-smime/src/test/resources/key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBANu2drKTZw3HX3fi0l4yAG5RV/nwl4poPOR+03xHvtQSedLX1CSg\nDC6ChkDJ4fqcNMX8LGCSgspj1d0b8gKQDu0CAwEAAQJAYlXkAkDe2tfk7q9iIC6Y\n6scVbRQ1fwjwWAQ7e2BRFHDsLNdTvFacwGAhHci83DVIwq6kl7MudUNhXGYi2yvt\nMQIhAOnwrcQvFc/g/RkFmWjcKFfzsZGYO2tP2vW4CqDhoFGzAiEA8G5Vw/Yl25vs\nQrF8RqouarLghHRG+qN4BpVYQwxZjN8CIQDKRVBpdXC9mcIc1WuMb/bt/QYGZgLS\nSWx/0s5VxmAQ4wIgIDT3gi+X9KoXZPu3hRPI8fwSPUwCMhLxwhgBYcHmwQsCIDX5\nIpHdsed3vjY69bjID8LgkIK7BUDGeIkQTMLxKtGb\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/valid-signer-nosan-privkey.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDYEMlw6YXHpO5b\nnyuk7gVdu8XtmhKkg6LhhZ9KYPstIdSr7EbH++h2KMVPVjprvijh35d5hMf9fiMU\n71A93NkO4by3fOIUAJy2m1oXLBSOBhglAbI+Z+DeGfULPvari0Cp6A4TAvvw0DZh\nwR3qzHS50AXzQLVAeL+QoEwOCP0A7I3Ihn+sE3wJlKR+Sk9hYj30WX8M65ievuQw\n3VNRZLX3TqY+oGyWHVhXKnp90ZovoN2Uhj5kTlz0GFJP+37t4urt+cHkoH60O8cP\nWqcvznwr7keN9z0xRhPJ98OIlob3MeQQqLfN3QQZxp/Wv6WyRGoQg6pE6MtIOKZP\nlDgK4cJhAgMBAAECggEAEZOKA4KnpJY84pyn5P6M1rNt9jZkolfoAd8IFnmdrS31\nmje6CU4reqM1686IqZeaRUeWT6cWyr7+VRdjqGilCqIf4zBIRtbG6M7p7P0jveru\nf2IsMQnrv72OUsggMlO9YqTzMiY5vvz9E4YtbBqOO0haF4/xvqkj8jyr+y9Nf4vY\nYag2EUddUX2Giqln97aE8G2O8+LJzXTIk1K15DfyBcq1kkye8OSkjsnLybP4wo+v\nuRSDbReF//j+MDepP6IZNPBVyEDyjaXFrNmVb+USyHtzKXwl/GwoYjN7gtOtqc1V\n8vVj1H6+RJK1E67ORLctzzMUZfG6vDB2UZ+vv0lqnQKBgQD7sYWfq1rMzUNhVX4D\nef0AOoQlBpnOiwoc+7JEXoZoKBw5xrTAxsKRtJFSgIYDYbfi2y3XLu5IO69nkao6\nEqArzaGn+Uc5JcCw4GVVoe6q6W4SwTGC690d/D7R2h24CYjqrIXkFwtvvM6f+5gl\nVu8da1eXbH73JAEigkKtrvQqVQKBgQDbwzTL6E6hY6DXAxsWzSp1d1n/SK6NdByX\n2yfCQRAQVX6KPVo8Gw1ZGH3IJ/KNVL/TXzDV3NfY6cYBbydcu0/EcVBS11+3KhT7\nuB+Y6T3RThwzRDRBlaeJtUmN63JWmCOBiMIRPmJU2uxS3JzNMqC5QekkjRb/7S2J\n300hAdlb3QKBgGkt3zRBTFmHcZ/sNRPI15RP38cFQiMQ8XH5MJ7njW1bTahLRF/G\n76op9gyvDtG89TZE95wTzZm772ntcmCARhToAqUKQ9w6zZJcw5wMZotfrxMBTupy\nHF4aejoB1yeAPIos/Gq7wpi4IvSyE/uOn7AAmoL54PjwP9Um8Cxaj0hdAoGBALQf\np6KJ4gj989LHxOhHeUmWbbmEBS4DwXvmMQxS76uzp2f/KXqiYappHI91zqRwllnV\nZ92iiXhNA/Ig/Q5QqOzGQ6Piy50BbPl0zNE0O2rWrt6GRJ6M3ylL4eHk3W6EfHWr\ndgVUMJyEY7b3A75chMfTchh3XCaga/bZhApNza4xAoGAZF1mFIowaUog23TWiann\nZuCrZObtd2bk4LdtUYSAs2oUFSEWOR4d/av+eb68yf+n69gk1MrP4BA3hyQOX1M9\nCz/9x/1Tewytywi36noxlgvTfAe3U93LxGjob4z2bsGQ0Xt8q4ngIrEvDeBQa0cq\nbUOxUekM/49+AAToLiTrZhM=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/valid-signer-nosan.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIID2TCCAsGgAwIBAgIUT0oQs1tpYdA/PUXwyeZqLP4DACYwDQYJKoZIhvcNAQEL\nBQAwfDELMAkGA1UEBhMCWFgxEjAQBgNVBAcMCUFjbWUgQ2l0eTEeMBwGA1UECgwV\nQWNtZSBDZXJ0aWZpY2F0ZXMgTHRkMRQwEgYDVQQDDAtleGFtcGxlLmNvbTEjMCEG\nCSqGSIb3DQEJARYUdmFsaWQtY2FAZXhhbXBsZS5jb20wHhcNMjIxMTA0MTI1NTM5\nWhcNMzIxMTAxMTI1NTM5WjB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBD\naXR5MR4wHAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4\nYW1wbGUuY29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANgQyXDphcek7lufK6TuBV27\nxe2aEqSDouGFn0pg+y0h1KvsRsf76HYoxU9WOmu+KOHfl3mEx/1+IxTvUD3c2Q7h\nvLd84hQAnLabWhcsFI4GGCUBsj5n4N4Z9Qs+9quLQKnoDhMC+/DQNmHBHerMdLnQ\nBfNAtUB4v5CgTA4I/QDsjciGf6wTfAmUpH5KT2FiPfRZfwzrmJ6+5DDdU1FktfdO\npj6gbJYdWFcqen3Rmi+g3ZSGPmROXPQYUk/7fu3i6u35weSgfrQ7xw9apy/OfCvu\nR433PTFGE8n3w4iWhvcx5BCot83dBBnGn9a/pbJEahCDqkToy0g4pk+UOArhwmEC\nAwEAAaNTMFEwHQYDVR0OBBYEFEaAxnUaGT+hDny105TcVjw00ZqfMB8GA1UdIwQY\nMBaAFEaAxnUaGT+hDny105TcVjw00ZqfMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI\nhvcNAQELBQADggEBAL1SPWI/JS9HL3TWN0Q/xkP7UAhgQM6BUzgyVMJ+MxPy34l3\n7vEsSYc3KY8ynNo/psbsBodDqq2iDthZKTtt/l9YAnQrfgpRP+pzLPFDKOB9cDGG\nBoY5KC77B/TXuTfvdT7y0Bvey6C7KMvmPecLNt1sqlCdoOwOyLJN7+zCWVR9nfd4\nY27VkOvt/zz+cycLmxMIgwGL8nDg37wIoPftEASMvrhO18ZxISsRyabzd6fDZi8m\nu1x97/djezOgoqMgasPBy+yOZbeRwamv1PVElTDJovb/rRS2WMne6+2nMOoQezA3\nC5rRLtPAoKPPStt6E5tSQSWa42VBW2HGD/n4ros=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/valid-signer-privkey.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDNPJbVVGb6ZAdh\nTmFlaDOCs9GRHMwuUugJqaVGzkOMZ/fMimj1r/XubrRh8hcEmu4iZQFfFrmWnRmp\nprK3L6dk6lLTAFlKpMu6yVnRa7LLJbYMQE07uMtbnxwyfwx8DAxiMNzG25s1vswA\nb2JlGPrCfq0oKxe1hSAAik2HI8+0O4Rnl1/V5w6e+Vba874zDK0zwMsu3CiURP+0\n/M5c8cOLIqOKPTaPInUOVnGm0q52IpoF5jVZj9b8+lLp8ayRMlBz9tk+ATc4//1j\n1jW7NlO6WQ7dHoyUs10tqbfb96Gkm9JbJ9JtOjILZ6O9pehEKE8VwCZOmoDQc+CH\nyy52JLXhAgMBAAECggEAT/IIjSvV+zYou8o03TQEUKbr/Ls7e9X2pgDrrROes1wy\nZf4KWZ3Dzi9YW4jaV4RkO4idyqUHAPjMLM4O8pWA/qnaPm/12EIuS+Gv94gcus5D\nRi1sCFX4/QUTDkZ4Hf/xePQwo9Oad4qNW6QHr3rV/xoqKCn1D9O9/gfhoEEeYMVV\nnPdHlRR1QPcbptsBGkMYrZ8LwjQb8rMvNTK8HRhc4cKrkobEFvwXcWNO9aIlLveP\nQ58TlQUijeu4eZPM9c3vjl8rM7Oic/ftuU6jmt9IayAVoYblw7yzeEk3lnBlPzME\ne01hr+QD6ZJiquO1stdm6a7TJv15lAghhTBysy7oawKBgQDcY+PBZByHOZ5GzDBb\nfwcScm/RM5zMfDJKrYLNehawJ5egyUOeRm+Ym8d7PpCOWrHLURLpxak2Nrsmivf8\ng1OYAFnWWQm6j3GmlnEspp65n/UVPHETq7tvqulOHmvcOVROsSgHWu8JMgk8mlK1\nkEKlw3y5yzIB7SdyUhCjY5UMGwKBgQDuZeWfg44qvi9o3bb0zbjAHgh0hCQjHy/G\nNVcaVXx06j0a1tAFyESmJqtCEbScrInZnrmpXmTvX5UQczWHi4RYMI8NNFmf3+i0\nXHxWICFrQhdkxppvrEJeXbdfws+UJT8A9K8kokYRO3WyDzrngddfvCbYgHQjMly+\n3Ke8oS2tswKBgQC0sf2ZoSA2ysn3mBCJ5AODX2pIZv3HNojxa4OUPuZ9NWj/fiS/\nj1aOFCMg7DIPVVLytR1BqDtNZOBbAJPEaFRQivEdalEssdFn2W8fQdlfrkN+TtkT\nXLlIHCQ/VXfvzt1Ny7hbF3Zm3qxuEMWBca8DQ91uY6gzpiKye5CCtfINQwKBgQDh\nEPIoFlsxnzvDFQ6VL2MsfS4eUmKLhfXkepcxFWPaPQpTPFpIGzo0Ym1sgqqw/3Nl\nMKS3cZZ5JxPj4+C1htH7MFzdan7yoMFhBa+c39itGkhbq+RBaa9+x5tHnPO8OS2y\nCU8QluLvgeMrp5VE2yAqEcfavernD7TfvBHf04r8YQKBgCHRqnBwHOGHiWyy/Wdu\nySxqN1hfJWBdKRbtxTj5uCKp/orNtiRVB4GC1ZqhCJ38rZwMSSbp00o9nyDXlRfP\njTo2zHrxtwem5hB31cZalPt4twJ5SofmNLDngz7UxwpTs8jWaAf8yAtRgdxWbYRg\nbRbZ+LceWQUl8T/RC7UQpy0Y\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "acme4j-smime/src/test/resources/valid-signer.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIID+jCCAuKgAwIBAgIUKAv8VHC4XCtd7BkomMblD3++fLQwDQYJKoZIhvcNAQEL\nBQAwfDELMAkGA1UEBhMCWFgxEjAQBgNVBAcMCUFjbWUgQ2l0eTEeMBwGA1UECgwV\nQWNtZSBDZXJ0aWZpY2F0ZXMgTHRkMRQwEgYDVQQDDAtleGFtcGxlLmNvbTEjMCEG\nCSqGSIb3DQEJARYUdmFsaWQtY2FAZXhhbXBsZS5jb20wHhcNMjIxMTA0MTI1NTM5\nWhcNMzIxMTAxMTI1NTM5WjB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBD\naXR5MR4wHAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4\nYW1wbGUuY29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM08ltVUZvpkB2FOYWVoM4Kz\n0ZEczC5S6AmppUbOQ4xn98yKaPWv9e5utGHyFwSa7iJlAV8WuZadGammsrcvp2Tq\nUtMAWUqky7rJWdFrsssltgxATTu4y1ufHDJ/DHwMDGIw3MbbmzW+zABvYmUY+sJ+\nrSgrF7WFIACKTYcjz7Q7hGeXX9XnDp75VtrzvjMMrTPAyy7cKJRE/7T8zlzxw4si\no4o9No8idQ5WcabSrnYimgXmNVmP1vz6UunxrJEyUHP22T4BNzj//WPWNbs2U7pZ\nDt0ejJSzXS2pt9v3oaSb0lsn0m06Mgtno72l6EQoTxXAJk6agNBz4IfLLnYkteEC\nAwEAAaN0MHIwHQYDVR0OBBYEFEdIEWKDgjyWKhYjxsAdeNVUwl7iMB8GA1UdIwQY\nMBaAFEdIEWKDgjyWKhYjxsAdeNVUwl7iMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0R\nBBgwFoEUdmFsaWQtY2FAZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBACQ0\nLDU1sVSzRwa1ZeqeiHrc3gbeuBqFqejj+QvPx2N3Zc53265+mb1ga1bJ3O1WePa/\nK/0L0Ska5Y5CELjcGlvy5kcSdQFRhoqK7KZq3c9C39ljl5yx//uJUauXWq+WJhqo\nvTKNo+QC4P9jIjtT6J7irNXl/bS2Qgnmp1xXsVNKmnhWXpbpyelFJgV6WhjtUHxA\nnDZ72q7naLG3tb3umBgAZ/DKQGPvM3WJ2BzwfD6I4c75O//DzB6D7p4w295WU/m/\nAQ5jc/hiEglEh2JHStq1ofvMKg90+azrc06gD7kjXgzaHFBesM8Xph1qrz+8jN1a\nF2L5+xQODQmvZhN5PDM=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme4j-smime/tool/smime-generator.py",
    "content": "#!/bin/env python3\n#\n# acme4j - Java ACME client\n#\n# Copyright (C) 2022 Richard \"Shred\" Körber\n#   http://acme4j.shredzone.org\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n#\n\n#\n# This tool creates ACME test e-mails and signs them. It can be\n# used to generate S/MIME mails for unit tests.\n#\n# Requires: M2Crypto\n#\n# WARNING: DO NOT USE THIS CODE TO GENERATE REAL S/MIME MAILS!\n#   This generator is only meant to create test mails for unit test\n#   purposes, and may lack security relevant features that are\n#   needed for real S/MIME mails.\n#\n\nfrom M2Crypto import BIO, Rand, SMIME\n\ndef makebuf(text):\n    return BIO.MemoryBuffer(bytes(text, 'UTF-8'))\n\ndef signmail(text, sender, recipient, subject, privkey, pubkey,\n             envelopeFrom=None, envelopeTo=None, envelopeSubject=None):\n    body = 'Content-Type: message/RFC822; forwarded=no\\r\\n\\r\\n'\n    body += 'From: {}\\r\\n'.format(sender)\n    body += 'To: {}\\r\\n'.format(recipient)\n    body += 'Subject: {}\\r\\n'.format(subject)\n    body += 'Message-ID: <A2299BB.FF7788@example.org>\\r\\n'\n    body += 'MIME-Version: 1.0\\r\\n'\n    body += 'Content-Type: text/plain; charset=utf-8\\r\\n'\n    body += '\\r\\n'\n    body += text\n    body += '\\r\\n'\n\n    s = SMIME.SMIME()\n    s.load_key(privkey, pubkey)\n    p7 = s.sign(makebuf(body), SMIME.PKCS7_DETACHED)\n\n    out = BIO.MemoryBuffer()\n    out.write('From: {}\\r\\n'.format(envelopeFrom if envelopeFrom is not None else sender))\n    out.write('To: {}\\r\\n'.format(envelopeTo if envelopeTo is not None else recipient))\n    out.write('Subject: {}\\r\\n'.format(envelopeSubject if envelopeSubject is not None else subject))\n    out.write('Auto-Submitted: auto-generated; type=acme\\r\\n')\n    out.write('Message-ID: <A2299BB.FF7788@example.org>\\r\\n')\n    s.write(out, p7, makebuf(body))\n\n    return out.read()\n\nwith open('src/test/resources/email/valid-mail.eml', 'wb') as w:\n    w.write(signmail('This is an automatically generated ACME challenge.',\n        'valid-ca@example.com',\n        'recipient@example.org',\n        'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',\n        'src/test/resources/valid-signer-privkey.pem',\n        'src/test/resources/valid-signer.pem'))\n\nwith open('src/test/resources/email/invalid-cert-mismatch.eml', 'wb') as w:\n    w.write(signmail('This is an automatically generated ACME challenge.',\n        'different-ca@example.com',\n        'recipient@example.org',\n        'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',\n        'src/test/resources/valid-signer-privkey.pem',\n        'src/test/resources/valid-signer.pem',\n        envelopeFrom=\"different-ca@example.org\"))\n\nwith open('src/test/resources/email/invalid-nosan.eml', 'wb') as w:\n    w.write(signmail('This is an automatically generated ACME challenge.',\n        'valid-ca@example.com',\n        'recipient@example.org',\n        'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',\n        'src/test/resources/valid-signer-nosan-privkey.pem',\n        'src/test/resources/valid-signer-nosan.pem'))\n\nwith open('src/test/resources/email/invalid-signed-mail.eml', 'wb') as w:\n    w.write(signmail('This is an automatically generated ACME challenge.',\n        'valid-ca@example.com',\n        'recipient@example.org',\n        'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',\n        'src/test/resources/invalid-signer-privkey.pem',\n        'src/test/resources/invalid-signer.pem'))\n\nwith open('src/test/resources/email/invalid-protected-mail-from.eml', 'wb') as w:\n    w.write(signmail('This is an automatically generated ACME challenge.',\n        'valid-ca@example.com',\n        'recipient@example.org',\n        'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',\n        'src/test/resources/valid-signer-privkey.pem',\n        'src/test/resources/valid-signer.pem',\n        envelopeFrom=\"tampered-ca@example.org\"))\n\nwith open('src/test/resources/email/invalid-protected-mail-to.eml', 'wb') as w:\n    w.write(signmail('This is an automatically generated ACME challenge.',\n        'valid-ca@example.com',\n        'recipient@example.org',\n        'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',\n        'src/test/resources/valid-signer-privkey.pem',\n        'src/test/resources/valid-signer.pem',\n        envelopeTo=\"tampered-recipient@example.com\"))\n\nwith open('src/test/resources/email/invalid-protected-mail-subject.eml', 'wb') as w:\n    w.write(signmail('This is an automatically generated ACME challenge.',\n        'valid-ca@example.com',\n        'recipient@example.org',\n        'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',\n        'src/test/resources/valid-signer-privkey.pem',\n        'src/test/resources/valid-signer.pem',\n        envelopeSubject=\"ACME: aDiFfErEnTtOkEn\"))\n"
  },
  {
    "path": "acme4j-smime/tool/test-key-generator.sh",
    "content": "#!/bin/bash\n#\n# acme4j - Java ACME client\n#\n# Copyright (C) 2022 Richard \"Shred\" Körber\n#   http://acme4j.shredzone.org\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n#\n\n#\n# Generates test keys for S/MIME unit tests.\n#\n# WARNING: DO NOT USE THIS CODE FOR KEY GENERATION IN PRODUCTION\n# ENVIRONMENTS!\n#\n\nTARGET=src/test/resources/\n\nopenssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 3650 \\\n  -keyout \"$TARGET/valid-signer-privkey.pem\" -out \"$TARGET/valid-signer.pem\" \\\n  -subj \"/C=XX/L=Acme City/O=Acme Certificates Ltd/CN=example.com/emailAddress=valid-ca@example.com\" \\\n  -addext \"subjectAltName=email:valid-ca@example.com\"\n\nopenssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 3650 \\\n  -keyout \"$TARGET/valid-signer-nosan-privkey.pem\" -out \"$TARGET/valid-signer-nosan.pem\" \\\n  -subj \"/C=XX/L=Acme City/O=Acme Certificates Ltd/CN=example.com/emailAddress=valid-ca@example.com\"\n\nopenssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 3650 \\\n  -keyout \"$TARGET/invalid-signer-privkey.pem\" -out \"$TARGET/invalid-signer.pem\" \\\n  -subj \"/C=XX/L=Acme City/O=Emca Certificates Ltd/CN=example.com/emailAddress=invalid-ca@example.com\" \\\n  -addext \"subjectAltName=email:invalid-ca@example.com\"\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n *\n * acme4j - ACME Java client\n *\n * Copyright (C) 2015 Richard \"Shred\" Körber\n *   http://acme4j.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n *\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>org.shredzone.acme4j</groupId>\n    <artifactId>acme4j</artifactId>\n    <version>5.1.1-SNAPSHOT</version>\n    <packaging>pom</packaging>\n\n    <name>acme4j</name>\n    <description>ACME client for Java</description>\n    <url>https://acme4j.shredzone.org</url>\n    <inceptionYear>2015</inceptionYear>\n\n    <licenses>\n        <license>\n            <name>Apache License Version 2.0</name>\n            <url>LICENSE-APL.txt</url>\n        </license>\n    </licenses>\n    <scm>\n        <url>https://codeberg.org/shred/acme4j/</url>\n        <connection>scm:git:git@codeberg.org:shred/acme4j.git</connection>\n        <developerConnection>scm:git:git@codeberg.org:shred/acme4j.git</developerConnection>\n      <tag>HEAD</tag>\n  </scm>\n    <issueManagement>\n        <system>Codeberg</system>\n        <url>https://codeberg.org/shred/acme4j/issues</url>\n    </issueManagement>\n    <developers>\n        <developer>\n            <id>shred</id>\n            <name>Richard Körber</name>\n        </developer>\n    </developers>\n\n    <properties>\n        <bouncycastle.version>1.83</bouncycastle.version>\n        <httpclient.version>4.5.14</httpclient.version>\n        <jakarta.mail.version>2.0.5</jakarta.mail.version>\n        <jose4j.version>0.9.6</jose4j.version>\n        <junit.version>5.14.3</junit.version>\n        <slf4j.version>2.0.17</slf4j.version>\n        <project.build.sourceEncoding>utf-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>utf-8</project.reporting.outputEncoding>\n    </properties>\n\n    <modules>\n        <module>acme4j-client</module>\n        <module>acme4j-smime</module>\n        <module>acme4j-example</module>\n        <module>acme4j-it</module>\n    </modules>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>3.14.1</version>\n                <configuration>\n                    <release>17</release>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>com.github.spotbugs</groupId>\n                <artifactId>spotbugs-maven-plugin</artifactId>\n                <version>4.9.8.2</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>check</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>3.5.4</version>\n                <configuration combine.children=\"append\">\n                    <parallel>classes</parallel>\n                    <threadCount>10</threadCount>\n                    <excludedGroups>requires-network</excludedGroups>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-failsafe-plugin</artifactId>\n                <version>3.5.4</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>integration-test</goal>\n                            <goal>verify</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-jar-plugin</artifactId>\n                <version>3.5.0</version>\n                <configuration>\n                    <excludes>\n                        <exclude>**/.gitignore</exclude>\n                    </excludes>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-release-plugin</artifactId>\n                <version>3.3.1</version>\n                <configuration>\n                    <autoVersionSubmodules>true</autoVersionSubmodules>\n                    <tagNameFormat>v@{project.version}</tagNameFormat>\n                    <pushChanges>false</pushChanges>\n                    <localCheckout>true</localCheckout>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-source-plugin</artifactId>\n                <version>3.4.0</version>\n                <executions>\n                    <execution>\n                        <id>attach-sources</id>\n                        <goals>\n                            <goal>jar</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-gpg-plugin</artifactId>\n                <version>3.2.8</version>\n                <executions>\n                    <execution>\n                        <id>sign-artifacts</id>\n                        <phase>deploy</phase>\n                        <goals>\n                            <goal>sign</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.sonatype.central</groupId>\n                <artifactId>central-publishing-maven-plugin</artifactId>\n                <version>0.10.0</version>\n                <extensions>true</extensions>\n                <configuration>\n                    <publishingServerId>central</publishingServerId>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-javadoc-plugin</artifactId>\n                <version>3.12.0</version>\n                <configuration>\n                    <doclint>syntax,reference</doclint>\n                    <linksource>true</linksource>\n                    <locale>en</locale>\n                    <tags>\n                        <tag>\n                            <name>draft</name>\n                            <placement>a</placement>\n                            <head>Draft:</head>\n                        </tag>\n                    </tags>\n                </configuration>\n                <executions>\n                    <execution>\n                        <id>attach-javadocs</id>\n                        <goals>\n                            <goal>jar</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.shredzone.maven</groupId>\n                <artifactId>mkdocs-maven-plugin</artifactId>\n                <version>1.2</version>\n                <configuration>\n                    <outputDirectory>${project.build.directory}/site</outputDirectory>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n    <dependencies>\n        <dependency>\n            <groupId>com.github.spotbugs</groupId>\n            <artifactId>spotbugs-annotations</artifactId>\n            <version>4.9.8</version>\n            <optional>true</optional>\n            <exclusions>\n                <exclusion>\n                    <groupId>com.google.code.findbugs</groupId>\n                    <artifactId>jsr305</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter</artifactId>\n            <version>${junit.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.assertj</groupId>\n            <artifactId>assertj-core</artifactId>\n            <version>3.27.7</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>net.javacrumbs.json-unit</groupId>\n            <artifactId>json-unit-assertj</artifactId>\n            <version>5.1.1</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <version>5.23.0</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <!-- Pinned to 3.2.0 until https://github.com/wiremock/wiremock/issues/2480 is resolved -->\n            <groupId>org.wiremock</groupId>\n            <artifactId>wiremock</artifactId>\n            <version>3.2.0</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n</project>\n"
  },
  {
    "path": "src/doc/docs/ca/actalis.md",
    "content": "# Actalis\n\nWebsite: [Actalis](https://www.actalis.com)\n\nAvailable since acme4j 4.0.0\n\n## Connection URIs\n\n* `acme://actalis.com` - Production server\n\nActalis does not provide a staging server (as of August 2025).\n\n## Note\n\n* Actalis requires account creation with [key identifier](../usage/account.md#external-account-binding).\n\n## Disclaimer\n\n_acme4j_ is not officially supported or endorsed by Actalis. If you have _acme4j_ related issues, please do not ask them for support, but [open an issue here](https://codeberg.org/shred/acme4j/issues).\n"
  },
  {
    "path": "src/doc/docs/ca/google.md",
    "content": "# Google\n\nWebsite: [Google Trust Services](https://pki.goog/)\n\nAvailable since acme4j 3.5.0\n\n## Connection URIs\n\n* `acme://pki.goog` - Production server\n* `acme://pki.goog/staging` - Staging server\n\n## Note\n\n_Google Trust Services_ requires account creation with [External Account Binding](../usage/account.md#external-account-binding). See [this tutorial](https://cloud.google.com/certificate-manager/docs/public-ca-tutorial) about how to create the EAB secrets. You will get a `keyId` and a `b64MacKey` that can be directly passed into `AccountBuilder.withKeyIdentifier()`.\n\n!!! note\n    You cannot use the production EAB secrets for accessing the staging server, but you need separate secrets! Please read the respective chapter of the tutorial about how to create them.\n\n_Google Trust Services_ request `HS256` as MAC algorithm. If you use the connection URIs above, this is set automatically. If you use a `https` connection URI, you will need to set the MAC algorithm manually by adding `withMacAlgorithm(\"HS256\")` to the `AccountBuilder`.\n\n## Disclaimer\n\n_acme4j_ is not officially supported or endorsed by Google. If you have _acme4j_ related issues, please do not ask them for support, but [open an issue here](https://codeberg.org/shred/acme4j/issues).\n"
  },
  {
    "path": "src/doc/docs/ca/index.md",
    "content": "# Certificate Authorities\n\n_acme4j_ should support any CA that is providing an ACME server.\n\n## Available Providers\n\n!!! note\n    _acme4j_ is not limited to these providers. **You can always connect to any [RFC 8555](https://tools.ietf.org/html/rfc8555) compliant server** by passing the `URL` of its directory endpoint to the `Session`.\n\nThe _acme4j_ package contains these providers (in alphabetical order):\n\n* [Actalis](actalis.md)\n* [Google](google.md)\n* [Let's Encrypt](letsencrypt.md)\n* [Pebble](pebble.md)\n* [SSL.com](sslcom.md)\n* [ZeroSSL](zerossl.md)\n\nMore CAs may be supported in future releases of _acme4j_.\n\nAlso, CAs can publish provider jar files that plug into _acme4j_ and offer extended support.\n\n## Disclaimer\n\n_acme4j_ is an independent software, and not officially supported or endorsed by any of these CAs. If you have _acme4j_ related issues, please do not ask them for support, but [open an issue here](https://codeberg.org/shred/acme4j/issues).\n"
  },
  {
    "path": "src/doc/docs/ca/letsencrypt.md",
    "content": "# Let's Encrypt\n\nWebsite: [Let's Encrypt](https://letsencrypt.org)\n\n## Connection URIs\n\n* `acme://letsencrypt.org` - Production server\n* `acme://letsencrypt.org/staging` - Testing server\n\n## Note\n\n* Let's Encrypt does not support `Account.getOrders()`, although it's required by RFC8555. Invocation will throw an `AcmeNotSupportedException`. See [this issue](https://github.com/letsencrypt/boulder/issues/3335) for more information."
  },
  {
    "path": "src/doc/docs/ca/pebble.md",
    "content": "# Pebble\n\n[Pebble](https://github.com/letsencrypt/pebble) is a small ACME test server.\n\nThis ACME provider can be used to connect to a local Pebble server instance, mainly for running integration tests.\n\n## Connection URIs\n\n* `acme://pebble` - Connect to a Pebble server at `localhost` and standard port 14000.\n* `acme://pebble:12345` - Connect to a Pebble server at `localhost` and port 12345.\n* `acme://pebble/pebble.example.com` - Connect to a Pebble server at `pebble.example.com` and standard port 14000.\n* `acme://pebble/pebble.example.com:12345` - Connect to a Pebble server at `pebble.example.com` and port 12345.\n\nPebble uses a self-signed certificate for HTTPS connections. The Pebble provider accepts this certificate.\n\n## Different Host Name\n\nThe Pebble server provides an end-entity certificate for the `localhost` and `pebble` domain.\n\nIf your Pebble server can be reached at a different domain (like `pebble.example.com` above), you need to create a correct end-entity certificate on your Pebble server. [See here](https://github.com/letsencrypt/pebble/tree/main/test/certs) for how to use `minica` to create a matching certificate, and the section below for how to use it in your integration tests.\n\nOtherwise, you will get an `AcmeNetworkException: Network error` that is caused by a `java.io.IOException: No subject alternative DNS name matching [...] found` when trying to access the Pebble server.\n\nIf you cannot create a correct end-entity certificate on your Pebble server, you could also globally disable host name verification on Java side: `-Djdk.internal.httpclient.disableHostnameVerification`\n\n!!! warning\n    **Disable hostname verification for testing purposes only, never in a production environment!** Create a correct end-entity certificate whenever possible.\n\n## Custom CA Certificate\n\nPebble provides a default CA certificate, which can be found at `test/certs/pebble.minica.pem` of the Pebble server. This default CA is integrated into _acme4j_'s Pebble provider, and is automatically accepted.\n\nIf you run a Pebble instance with a custom `pebble.minica.pem`, copy your PEM file as a resource to your project (either in the `src/test/resources` or `src/test/resources/META-INF` folder).\n"
  },
  {
    "path": "src/doc/docs/ca/sslcom.md",
    "content": "# SSL.com\n\nWebsite: [SSL.com](https://ssl.com)\n\nAvailable since acme4j 3.2.0. **This provider is experimental!**\n\n## Connection URIs\n\n* `acme://ssl.com`, `acme://ssl.com/ecc` - Production server, ECDSA certificate mode\n* `acme://ssl.com/rsa` - Production server, RSA certificate mode\n* `acme://ssl.com/staging`, `acme://ssl.com/staging/ecc` - Testing server, ECDSA certificate mode\n* `acme://ssl.com/staging/rsa` - Testing server, RSA certificate mode\n\n## Note\n\n* This CA requires [External Account Binding (EAB)](../usage/account.md#external-account-binding) for account creation. However, the CA's directory resource returns `externalAccountRequired` as `false`, which is incorrect. If you use one of the `acme:` URIs above, _acme4j_ will patch the metadata transparently. If you directly connect to SSL.com via `https:` URI though, `Metadata.isExternalAccountRequired()` could return a wrong value. (As of February 2024)\n* The certificate of the ssl.com staging server seems to be unmonitored. When it expires, an `AcmeNetworkException` is thrown which is caused by a `CertificateExpiredException`. There is nothing you can do to fix this error, except to ask the ssl.com support to renew the expired certificate on their server. **Please do not open an issue at acme4j.** (As of June 2024)\n\n## Disclaimer\n\n_acme4j_ is not officially supported or endorsed by SSL.com. If you have _acme4j_ related issues, please do not ask them for support, but [open an issue here](https://codeberg.org/shred/acme4j/issues).\n"
  },
  {
    "path": "src/doc/docs/ca/zerossl.md",
    "content": "# ZeroSSL\n\nWebsite: [ZeroSSL](https://zerossl.com)\n\nAvailable since acme4j 3.2.0. **This provider is experimental!**\n\n## Connection URIs\n\n* `acme://zerossl.com` - Production server\n\nZeroSSL does not provide a staging server (as of February 2024).\n\n## Note\n\n* ZeroSSL requires account creation with [key identifier](../usage/account.md#external-account-binding).\n* ZeroSSL makes use of the retry-after header, so expect that the `fetch()` methods return an `Instant`, and wait until this moment has passed (see [example](../example.md)).\n* Certificate creation can take a considerable amount of time (up to 24h). The retry-after header still gives a short retry period, resulting in a very high number of status update reattempts.\n* Server response can be very slow sometimes. If there are frequent timeouts, you can increase the duration in the [network settings](../usage/advanced.md#network-settings).\n\n!!! note\n    If you have used the [example code](../example.md) of _acme4j_ before version 3.2.0, please review the updated example for how to use ZeroSSL with _acme4j_.\n\n## Disclaimer\n\n_acme4j_ is not officially supported or endorsed by ZeroSSL. If you have _acme4j_ related issues, please do not ask them for support, but [open an issue here](https://codeberg.org/shred/acme4j/issues).\n"
  },
  {
    "path": "src/doc/docs/challenge/dns-01.md",
    "content": "# dns-01 Challenge\n\nWith the `dns-01` challenge, you prove to the CA that you are able to control the DNS records of the domain to be authorized, by creating a TXT record with a signed content.\n\n`Dns01Challenge` provides a resource record name and a digest string:\n\n```java\nDns01Challenge challenge = auth.findChallenge(Dns01Challenge.class);\n\nString resourceName = challenge.getRRName(auth.getIdentifier());\nString digest = challenge.getDigest();\n```\n\nThe CA expects a TXT record at `resourceName` with the `digest` string as value. The `Dns01Challenge.getRRName()` method converts the domain name to a resource record name (including the trailing full stop, e.g. `_acme-challenge.www.example.org.`).\n\nThe validation was successful if the CA was able to fetch the TXT record and got the correct `digest` returned.\n"
  },
  {
    "path": "src/doc/docs/challenge/dns-account-01.md",
    "content": "# dns-account-01 Challenge\n\nWith the `dns-account-01` challenge, you prove to the CA that you are able to control the DNS records of the domain to be authorized, by creating a TXT record with a signed content.\n\nThis challenge is specified in [draft-ietf-acme-dns-account-label-02](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-account-label/).\n\n!!! warning\n    The support of this challenge is **experimental**. The implementation is only unit tested for compliance with the specification, but is not integration tested yet. There may be breaking changes in this part of the API in future releases. Semantic versioning does not apply here.\n\n`DnsAccount01Challenge` provides a digest string and a resource record name:\n\n```java\nDnsAccount01Challenge challenge = auth.findChallenge(DnsAccount01Challenge.class);\n\nString resourceRecordName = challenge.getRRName(auth.getIdentifier());\nString digest = challenge.getDigest();\n```\n\nThe CA expects a TXT record at `resourceRecordName` with the `digest` string as value. The `getRRName()` method converts the domain name to a resource record name (including the trailing full stop).\n\nThe validation was successful if the CA was able to fetch the TXT record and got the correct `digest` returned.\n"
  },
  {
    "path": "src/doc/docs/challenge/dns-persist-01.md",
    "content": "# dns-persist-01 Challenge\n\nWith the `dns-persist-01` challenge, you prove to the CA that you are able to control the DNS records of the domain to be authorized, by creating a TXT record that refers to your account.\n\nIn contrast to the [`dns-01`](dns-01.md) challenge, the DNS TXT records are long-term and can be created manually, so your services do not need the access credentials of your DNS server.\n\nThis challenge is specified in [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/).\n\n!!! warning\n    The support of this challenge is **experimental**. The implementation is only unit tested for compliance with the specification, but is not integration tested yet. There may be breaking changes in this part of the API in future releases. Semantic versioning does not apply here.\n\n`DnsPersist01Challenge` provides a resource record name and a digest string:\n\n```java\nDnsPersist01Challenge challenge = auth.findChallenge(DnsPersist01Challenge.class);\n\nString resourceName = challenge.getRRName(auth.getIdentifier());\nString rdata = challenge.getRData();\n```\n\nThe CA expects a TXT record at `resourceName` with the `rdata` string as value. The `DnsPersist01Challenge.getRRName()` method converts the domain name to a resource record name (including the trailing full stop, e.g. `_validation-persist.www.example.org.`).\n\nThe `rdata` value is bound to your account at the CA. It stays the same for future challenges. If the TXT record is already set, you can skip ahead and trigger the challenge. However, most CAs will automatically authorize your domain if the TXT record exists, so you may not even need to complete the challenge.\n\nThe validation was successful if the CA was able to fetch the TXT record.\n\n## Additional parameters\n\nTo add further parameters to the RDATA, a builder can be used:\n\n```java\nString rdata = challenge.buildRData()\n        .issuerDomainName(\"ca.example.com\")\n        .wildcard()\n        .persistUntil(Instant.now().plus(3, ChronoUnit.MONTHS))\n        .noQuotes()\n        .build();\n```\n\n* `issuerDomainName` sets a different issuer domain name. It must be one of `DnsPersist01Challenge.getIssuerDomainNames()`. If not set, the first issuer domain name of that list is taken as default.\n* `wildcard` marks that you want to authorize wildcard.\n* `persistUntil` limits the validity of the TXT record. If not set, the TXT record will be valid indefinitely.\n* `noQuotes` generates RDATA without quote-enclosed strings. You _must_ then take care of long RDATA values yourself!\n\n`getRdata()` is just a shortcut for `buildRData().build()`.\n"
  },
  {
    "path": "src/doc/docs/challenge/email-reply-00.md",
    "content": "# email-reply-00 Challenge\n\nThe `email-reply-00` challenge permits to get end-user S/MIME certificates, as specified in [RFC 8823](https://tools.ietf.org/html/rfc8823).\n\nThe CA must support issuance of S/MIME certificates. _Let's Encrypt_ does not currently support it.\n\n!!! warning\n    The support of this challenge is **experimental**. The implementation is only unit tested for compliance with the RFC, but is not integration tested yet.\n\n## Setup and Requirements\n\nTo use the S/MIME support, you need to:\n\n* add the `acme4j-smime` module to your list of dependencies\n* make sure that `BouncyCastleProvider` is added as security provider\n\n[RFC 8823](https://tools.ietf.org/html/rfc8823) requires that the DKIM or S/MIME signature of incoming mails _must_ be checked. Outgoing mails _must_ have a valid DKIM signature. Starting with v2.15, _acme4j_ is able to validate and sign S/MIME verification mails. DKIM is usually done by the MTA and thus out of the scope of `acme4j-smime`.\n\n## Ordering\n\nThe certificate ordering process is similar to a standard domain certificate order.\n\nHowever, if `Identifier` objects are needed, use `EmailIdentifier.email()` to generate an identifier for the email address you want an S/MIME certificate for.\n\nTo generate a CSR, the module provides a `SMIMECSRBuilder` that works similar to the standard `CSRBuilder`, but accepts `EmailIdentifier` objects.\n\nWith the `SMIMECSRBuilder.setKeyUsageType()`, the desired usage type of the S/MIME certificate can be selected. By default, the certificate can be used both for encryption and signing. However, this is just a proposal, and the CA is free to ignore it or return an error if the desired usage type is not supported.\n\n## Challenge and Response\n\nThe CA validates ownership of the email address by two components.\n\nFirstly, the CA sends a challenge email to the email address that requested the S/MIME certificate. The subject of this email always starts with an `ACME:` prefix, so it can be filtered by the inbound MTA for automatic processing. After the prefix, the mail subject contains the first part of the challenge token (called \"Token 1\").\n\nSecondly, the CA provides a new `EmailReply00Challenge` challenge that needs to be verified by the client. The challenge contains the second part of the challenge token (called \"Token 2\"). Both token parts are concatenated to give the full token that is required for generating the key authorization. The `EmailReply00Challenge` class offers methods like `getToken(String part1)`, `getTokenPart2()`, and `getAuthorization(String part1)` for that.\n\nThe client now needs to generate a response to the request email. This is a standard mail response to the sender's address. The subject line must be kept, except of an optional `Re:` or a similar prefix. The mail body must contain a `text/plain` part that contains the wrapped key authorization string. For example:\n\n```text\n-----BEGIN ACME RESPONSE-----\nLoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowy\njxAjEuX0=\n-----END ACME RESPONSE-----\n```\n\nThis response is sent back to the CA.\n\nAfter that, the `EmailReply00Challenge` is triggered. The CA now has a proof of ownership of the email address, and can issue the S/MIME certificate.\n\n## Response Helper\n\nThe response process can be executed programmatically, or even manually. To help with the process, `acme4j-smime` provides an `EmailProcessor` that helps you to parse the challenge email, and generate a matching response mail.\n\nIt is basically invoked like this:\n\n```java\nMessage               challengeMessage = // incoming challenge message from the CA\nEmailReply00Challenge challenge        = // challenge that is requested by the CA\nEmailIdentifier       identifier       = // email address to get the S/MIME cert for\njakarta.mail.Session  mailSession      = // jakarta.mail session\n\nMessage response = EmailProcessor.plainMessage(challengeMessage)\n            .expectedIdentifier(identifier)\n            .withChallenge(challenge)\n            .respond()\n            .generateResponse(mailSession);\n\nTransport.send(response);   // send response to the CA\nchallenge.trigger();        // trigger the challenge\n```\n\nThe `EmailProcessor` and the related `ResponseGenerator` offer more methods for validating and for customizing the response email, see [the autodocs](../acme4j-smime/apidocs/org.shredzone.acme4j.smime/module-summary.html).\n\n## Validating S/MIME Challenge E-Mails\n\nThe `EmailProcessor` is able to validate challenge e-mails that were signed by the CA using S/MIME. To do so, invoke the processor like this:\n\n```java\nMessage               challengeMessage = // incoming challenge message from the CA\nEmailReply00Challenge challenge        = // challenge that is requested by the CA\nEmailIdentifier       identifier       = // email address to get the S/MIME cert for\n\nMessage response = EmailProcessor.signedMessage(challengeMessage)\n            .expectedIdentifier(identifier)\n            .withChallenge(challenge)\n            .respond()\n            .generateResponse();\n\nTransport.send(response);   // send response to the CA\nchallenge.trigger();        // trigger the challenge\n```\n\nIf you need more control of the signature verification process, you can use `EmailProcessor.builder()`. It is useful e.g. if you need to use a different trust store, or if your MTA has mangled the incoming message, so a relaxed verification is needed.\n"
  },
  {
    "path": "src/doc/docs/challenge/http-01.md",
    "content": "# http-01 Challenge\n\nWith the `http-01` challenge, you prove to the CA that you are able to control the website content of the domain to be authorized, by making a file with a signed content available at a given path.\n\n`Http01Challenge` provides two strings:\n\n```java\nHttp01Challenge challenge = auth.findChallenge(Http01Challenge.class);\nString domain = auth.getIdentifier().getDomain();\n\nString token = challenge.getToken();\nString content = challenge.getAuthorization();\n```\n\n`token` is the name of the file that will be requested by the CA server. It must contain the `content` string, without any leading or trailing white spaces or line breaks. The `Content-Type` header must be either `text/plain` or absent.\n\nThe expected path is (assuming that `${domain}` is the domain to be authorized, and `${token}` is the token):\n\n```\nhttp://${domain}/.well-known/acme-challenge/${token}\n```\n\nThe validation was successful if the CA was able to download that file and found `content` in it.\n\n!!! note\n    The request is sent to port 80 only, but redirects are followed. If your domain has multiple IP addresses, the CA randomly selects some of them. There is no way to choose a different port or a fixed IP address.\n\nYour server should be able to handle multiple requests to the challenge. The ACME server may check your response multiple times, and from different IPs. Also keep your response available until the `Authorization` status has changed to `VALID` or `INVALID`.\n"
  },
  {
    "path": "src/doc/docs/challenge/index.md",
    "content": "# Challenges\n\nChallenges are used to prove ownership of a domain.\n\nThere are different kinds of challenges. The most simple is maybe the HTTP challenge, where a file must be made available at the domain that is to be validated. It is assumed that you control the domain if you are able to publish a given file under a given path.\n\nThe ACME specifications define these standard challenges:\n\n* [dns-01](dns-01.md) ([RFC 8555](https://datatracker.ietf.org/doc/html/rfc8555#section-8.4), section 8.4)\n* [http-01](http-01.md) ([RFC 8555](https://datatracker.ietf.org/doc/html/rfc8555#section-8.3), section 8.3)\n\n_acme4j_ also supports these non-standard challenges:\n\n* [dns-account-01](dns-account-01.md) ([draft-ietf-acme-dns-account-label-02](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-account-label/))\n* [dns-persist-01](dns-persist-01.md) ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/))\n* [email-reply-00](email-reply-00.md) ([RFC 8823](https://tools.ietf.org/html/rfc8823))\n* [tls-alpn-01](tls-alpn-01.md) ([RFC 8737](https://tools.ietf.org/html/rfc8737))\n"
  },
  {
    "path": "src/doc/docs/challenge/tls-alpn-01.md",
    "content": "# tls-alpn-01 Challenge\n\nWith the `tls-alpn-01` challenge, you prove to the CA that you are able to control the web server of the domain to be authorized, by letting it respond to a request with a specific self-signed cert utilizing the ALPN extension. This challenge is specified in [RFC 8737](https://tools.ietf.org/html/rfc8737).\n\nYou need to create a self-signed certificate with the domain to be validated set as the only _Subject Alternative Name_. The byte array provided by `challenge.getAcmeValidation()` must be set as DER encoded `OCTET STRING` extension with the object id `1.3.6.1.5.5.7.1.31`. It is required to set this extension as critical.\n\n_acme4j_ does the heavy lifting for you though, and provides a certificate that is ready to use. It is valid for 7 days, which is ample of time to perform the validation.\n\n```java\nTlsAlpn01Challenge challenge = auth.findChallenge(TlsAlpn01Challenge.class);\nIdentifier identifier = auth.getIdentifier();\nKeyPair certKeyPair = KeyPairUtils.createKeyPair(2048);\n\nX509Certificate cert = challenge.createCertificate(certKeyPair, identifier);\n```\n\nNow use `cert` and `certKeyPair` to let your web server respond to TLS requests containing an ALPN extension with the value `acme-tls/1` and a SNI extension containing your subject (`identifier`).\n\nWhen the validation is completed, the `cert` and `certKeyPair` are not used anymore and can be disposed.\n\n!!! note\n    The request is sent to port 443 only. If your domain has multiple IP addresses, the CA randomly selects some of them. There is no way to choose a different port or a fixed IP address.\n\nYour server should be able to handle multiple requests to the challenge. The ACME server may check your response multiple times, and from different IPs. Also keep your response available until the `Authorization` status has changed to `VALID` or `INVALID`.\n"
  },
  {
    "path": "src/doc/docs/development/challenge.md",
    "content": "# ACME Challenge\n\nRFC 8555 only specifies the [`http-01`](../challenge/http-01.md) and [`dns-01`](../challenge/dns-01.md) challenges. _acme4j_ permits to add further challenge types, which are either generic or provider proprietary.\n\n## Provider Proprietary Challenges\n\nIf your provider requires a challenge that is too special for generic use, you can add it to your provider package and generate it via `createChallenge(Login, JSON)`. See the [Individual Challenges](provider.md#individual-challenges) section of the [ACME Provider](provider.md) chapter.\n\n## Generic Challenges\n\nStarting with _acme4j_ v2.12, generic challenges can be added globally using Java's `ServiceLoader` mechanism.\n\nYour implementation must provide a challenge provider that implements the `org.shredzone.acme4j.provider.ChallengeProvider` interface and is annotated with a `org.shredzone.acme4j.provider.ChallengeType` annotation giving the name of your challenge. The only method `Challenge create(Login login, JSON data)` must return a new instance of your `Challenge` class which is initialized with the challenge data given in the `data` JSON structure.\n\nA simple example of a `ChallengeProvider` is:\n\n```java\n@ChallengeType(\"my-example-01\")\npublic class MyExample01ChallengeProvider implements ChallengeProvider {\n    @Override\n    public Challenge create(Login login, JSON data) {\n        return new MyExample01Challenge(login, data);\n    }\n}\n```\n\nNote that you cannot replace predefined challenges, or another challenge implementation of the same type. If your `@ChallengeType` is already known to _acme4j_, an exception will be thrown on initialization time.\n\nThe `ChallengeProvider` implementation needs to be registered with Java's `ServiceLoader`. In the `META-INF/services` path of your project, create a file `org.shredzone.acme4j.provider.ChallengeProvider` and write the fully qualified class name of your implementation into that file. If you use Java modules, there must also be a `provides` section in your `module-info.java`, e.g.:\n\n```java\nprovides org.shredzone.acme4j.provider.ChallengeProvider\n    with org.example.acme.challenge.MyExample01ChallengeProvider;\n```\n\nThe `acme4j-smime` module is implemented that way, and also serves as an example of how to add generic challenges.\n\n## Adding your generic Challenge to _acme4j_\n\nAfter you completed your challenge code, you can send in a pull request and apply for inclusion in the _acme4j_ code base. If it is just a simple challenge implementation, you can apply for inclusion in the `acme4j-client` module. If the challenge is complex, or requires further dependencies, please create a separate module.\n\nThese preconditions must be met:\n\n* The challenge must be of common interest. If the challenge is only useful to your CA, better publish an own Java package instead.\n* The specification of the challenge must be available to the public. It must be downloadable free of charge and without prior obligation to register.\n* Your source code must be published under the terms of [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).\n* You have the permission of all trademark holders involved, to use their trademarks in the source codes, and package names.\n\nThe _acme4j_ development team reserves the right to reject your pull request, without giving any reason.\n\nIf you cannot meet these preconditions (or if your pull request was rejected), you can publish a JAR package of your _acme4j_ challenge yourself. Due to the plug-in nature of _acme4j_ challenges, it is sufficient to have that package in the Java classpath at runtime. There is no need to publish the source code.\n"
  },
  {
    "path": "src/doc/docs/development/index.md",
    "content": "# acme4j Development\n\nThis part of the documentation is addressed to developers who want to extend _acme4j_, or who want to use _acme4j_ for integration tests.\n\n* [Write an own Provider extension](provider.md)\n* [Integration Tests](testing.md)\n"
  },
  {
    "path": "src/doc/docs/development/provider.md",
    "content": "# ACME Provider\n\nBasically, it is possible to connect to any kind of ACME server just by connecting to the URL of its directory resource:\n\n```java\nSession session = new Session(\"https://api.example.org/directory\");\n```\n\nACME providers are \"plugins\" to _acme4j_ that are specialized on a single CA. The example above would then look like this (if the CA is supported by _acme4j_):\n\n```java\nSession session = new Session(\"acme://example.org\");\n```\n\n## Writing your own Provider\n\nEvery CA that provides an ACME server should also have an own `AcmeProvider`, and if it is just for the sake of a pretty `acme:` URI.\n\nHowever, it is also possible to adapt the behavior of wide parts of _acme4j_ to special characteristics of the CA, just by overriding methods and extending classes.\n\nA client provider implements the [`AcmeProvider`](../acme4j-client/apidocs/org.shredzone.acme4j/org/shredzone/acme4j/provider/AcmeProvider.html) interface, but usually it is easier to extend [`AbstractAcmeProvider`](../acme4j-client/apidocs/org.shredzone.acme4j/org/shredzone/acme4j/provider/AbstractAcmeProvider.html) and implement only these two methods:\n\n* `accepts(URI)` checks if the client provider is accepting the provided URI. Usually it would be a URI like `acme://example.com`. Note that the `http` and `https` schemes are reserved for the generic provider and cannot be used by other providers.\n* `resolve(URI)` parses the URI and returns the corresponding URL of the directory service.\n\nThe `AcmeProvider` implementation needs to be registered with Java's `ServiceLoader`. In the `META-INF/services` path of your project, create a file `org.shredzone.acme4j.provider.AcmeProvider` and write the fully qualified class name of your implementation into that file. If you use Java modules, there must also be a `provides` section in your `module-info.java`, e.g.:\n\n```java\nprovides org.shredzone.acme4j.provider.AcmeProvider\n    with org.example.acme.provider.MyAcmeProvider;\n```\n\nWhen _acme4j_ tries to connect to an acme URI, it first invokes the `accepts(URI)` method of all registered `AcmeProvider`s. Only one of the providers must return `true` for a successful connection. _acme4j_ then invokes the `resolve(URI)` method of that provider, and connects to the directory URL that is returned.\n\nThe connection fails if no or more than one `AcmeProvider` implementation accepts the acme URI.\n\n## Certificate Pinning\n\nThe standard Java mechanisms are used to verify the HTTPS certificate provided by the ACME server. To pin the certificate, or use a self-signed certificate, override the `createHttpConnector()` method of `AbstractAcmeProvider` and return a subclassed `HttpConnector` class that modifies the `HttpURLConnection` as necessary.\n\n## Individual Challenges\n\nIf your ACME server provides challenges that are not specified in the ACME protocol, there should be an own `Challenge` implementation for each of your challenge, by extending the [`Challenge`](../apidocs/org/shredzone/acme4j/challenge/Challenge.html) class.\n\nIn your `AcmeProvider` implementation, override the `createChallenge(Login, JSON)` method so it returns a new instance of your `Challenge` implementation when your individual challenge type is requested. All other types should be delegated to the super method.\n\n## Amended Directory Service\n\nTo override single entries of an ACME server's directory, or to use a static directory, override the `directory(Session, URI)` method, and return a `JSON` of all available resources and their respective URL.\n\n## Adding your Provider to _acme4j_\n\nAfter you completed your provider code, you can send in a pull request and apply for inclusion in the _acme4j_ code base.\n\nThese preconditions must be met:\n\n* Your provider's source code must be published under the terms of [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).\n* The source code of your ACME server must be publicly available under an [OSI compliant license](https://opensource.org/licenses/alphabetical).\n* To avoid name conflicts, the `acme:` URI used must have the official domain name of your service as domain part.\n* You have the permission of all trademark holders involved, to use their trademarks in the source codes, package names, and the acme URI.\n\nThe _acme4j_ development team reserves the right to reject your pull request, without giving any reason.\n\nIf you cannot meet these preconditions (or if your pull request was rejected), you can publish a JAR package of your _acme4j_ provider yourself. Due to the plug-in nature of _acme4j_ providers, it is sufficient to have that package in the Java classpath at runtime. There is no need to publish the source code.\n"
  },
  {
    "path": "src/doc/docs/development/testing.md",
    "content": "# Testing acme4j\n\n## Integration Tests\n\n_acme4j_ provides a number of integration tests. These tests are _not_ executed by maven by default, as they require Docker on the build machine.\n\nTo run them, install Docker and make it available to your user. Then invoke `mvn -Pci verify` to run the integration tests.\n\nThe integration tests use the [latest docker image](https://hub.docker.com/r/letsencrypt/pebble) of the [Pebble ACME test server](https://github.com/letsencrypt/pebble) in a container named _pebble_. A second docker container named _bammbamm_ uses [pebble-challtestsrv](https://hub.docker.com/r/letsencrypt/pebble-challtestsrv) as a test server. Pebble connects to this test server for resolving domain names and for the verification of challenges.\n\nIf you change into the `acme-it` module's directory, you can also build, start and stop the test servers with `mvn docker:build`, `mvn docker:start` and `mvn docker:stop`, respectively. While the test servers are running, you can run the integration tests in your IDE. `mvn docker:remove` finally removes the test server images.\n\nIf you like to change the default configuration of the integration tests (e.g. because you are running _pebble_ and _pebble-challtestsrv_ instances on a dedicated test server), you can do so by changing these system properties:\n\n* `pebbleHost`: Host name of the pebble server. Default: `localhost`\n* `pebblePort`: Port the pebble server is listening on. Default: 14000\n* `bammbammUrl`: URI of the _pebble-challtestsrv_ to connect to. Default: `http://localhost:8055`\n\n!!! warning\n    _pebble-challtestsrv_ is meant for testing purposes only. Only use it in secured testing environments. The server is neither hardened nor does it offer any kind of authentication.\n\n## Boulder\n\nIt is also possible to run some tests against the [Boulder](https://github.com/letsencrypt/boulder) ACME server, but the setup is a little tricky.\n\nFirst, build and start the integration test Docker containers as [explained above](#Integration_Tests). When the servers are started, find out the IP address of the _bammbamm_ server:\n\n```bash\ndocker inspect bammbamm -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'\n```\n\nNow set up a Docker instance of Boulder. Follow the instructions in the [Boulder README](https://github.com/letsencrypt/boulder#quickstart). When you are ready to start it, set the `FAKE_DNS` env variable to the IP address of _bammbamm_ you have found before.\n\nThe Boulder integration tests can now be run with `mvn -P boulder verify`.\n\nFor a local Boulder installation, just make sure that `FAKE_DNS` is set to `127.0.0.1`. You'll also need to expose the ports 5001 and 5002 of _bammbamm_ by changing the `acme4j-it/pom.xml` accordingly.\n"
  },
  {
    "path": "src/doc/docs/example.md",
    "content": "# Example\n\nFor a quick start, there is a simple example provided in the `acme4j-example` module. The example class is named `org.shredzone.acme4j.example.ClientTest`. It will demonstrate all the steps that are necessary for generating key pairs, authorizing domains, and ordering a certificate.\n\nThis chapter contains a copy of the class file, along with explanations about what is happening.\n\n## Caveats\n\n- The `ClientTest` is meant to be a simple example and proof of concept. It is not meant for production use as it is.\n\n- The exception handling is very simple. If an exception occurs during the process, the example will fail altogether. A real client should handle exceptions like `AcmeUserActionRequiredException` and `AcmeRateLimitedException` properly, by showing the required user action, or delaying the registration process until the rate limitation has been lifted, or the retry time has been reached.\n\n- At some places the example synchronously polls the server state. This is sufficient for simple cases, but a more complex client should use timers instead. The client should also make use of the fact that authorizations can be executed in parallel, shortening the certification time for multiple domains.\n\n- I recommend reading at least the chapters about [usage](usage/index.md) and [challenges](challenge/index.md), to learn more about how _acme4j_ and the ACME protocol works.\n\n- To make the example easier to understand, I will use the specific datatypes instead of the `var` keyword.\n\n## Configuration\n\n**The example won't run as-is.** You first need to set some constants according to the CA you intend to connect to. All constants can be found at the top of the class.\n\nThere is one constant that you **must** change in order to make the example work at all:\n\n* `CA_URI`: Set this constant to the connection URI of the CA you intend to use, see the [Connecting](usage/connecting.md) chapter. (The default value is just an example placeholder and won't work.)\n\nDepending on the requirements of your CA, you might also need to set these constants:\n\n* `ACCOUNT_EMAIL`: This is the email address that is connected to your account. The default is `null`, meaning that no email address is set. Some CAs accept that, but otherwise you can set your email address here.\n* `EAB_KID`, `EAB_HMAC`: If your CA requires External Account Binding (EAB), it will provide you with a KID and a HMAC pair that is connected to your account. In this case, you must provide both values in the corresponding constants (be careful not to mix them up). Otherwise, both constants must be set to `null`, which is the default and disables EAB.\n\nThe other constants should work with their default values, but can still be changed if necessary:\n\n* `ACCOUNT_KEY_SUPPLIER`: A function for generating a new account key pair. The default generates an EC key pair, but you can also use other `KeyPairUtils` methods (or entirely different means) to generate other kind of key pairs.\n* `DOMAIN_KEY_SUPPLIER`: A function for generating a new domain key pair. The default generates an RSA key pair, but you can also use other `KeyPairUtils` methods (or entirely different means) to generate other kind of key pairs.\n* `USER_KEY_FILE`: File name where the generated account key is stored. Default is `user.key`.\n* `DOMAIN_KEY_FILE`: File name where the generated domain key is stored. Default is `domain.key`.\n* `DOMAIN_CHAIN_FILE`: File name where the ordered domain certificate chain is stored. Default is `domain-chain.crt`.\n* `CHALLENGE_TYPE`: The challenge type you want to perform for domain validation. The default is `ChallengeType.HTTP` for [http-01](challenge/http-01.md) validation, but you can also use `ChallengeType.DNS` to perform a [dns-01](challenge/dns-01.md) validation. The example does not support other kind of challenges.\n* `TIMEOUT`: Maximum time until an expected resource status must be reached. Default is 60 seconds. If you get frequent timeouts with your CA, increase the timeout.\n\n## Running the Example\n\nAfter configuration, you can run the `ClientTest` class in your IDE, giving the domain names to be registered as parameters. When changing into the `acme4j-example` directory, the test client can also be invoked via maven in a command line:\n\n```sh\nmvn exec:java -Dexec.args=\"example.com example.org\"\n```\n\n## Invocation\n\nThe `main()` method performs a simple parameter check, and then invokes the `ClientTest.fetchCertificate()` method, giving a collection of domain names to get a certificate for.\n\n```java\npublic static void main(String... args) {\n    if (args.length == 0) {\n        System.err.println(\"Usage: ClientTest <domain>...\");\n        System.exit(1);\n    }\n\n    LOG.info(\"Starting up...\");\n\n    Security.addProvider(new BouncyCastleProvider());\n\n    Collection<String> domains = Arrays.asList(args);\n    try {\n        ClientTest ct = new ClientTest();\n        ct.fetchCertificate(domains);\n    } catch (Exception ex) {\n        LOG.error(\"Failed to get a certificate for domains \" + domains, ex);\n    }\n}\n```\n\n!!! note\n    The example requires the `BouncyCastleProvider` to be added as security provider.\n\n## The Main Workflow\n\nThe `fetchCertificate()` method contains the main workflow. It expects a collection of domain names.\n\n```java\npublic void fetchCertificate(Collection<String> domains)\n        throws IOException, AcmeException, InterruptedException {\n    // Load the user key file. If there is no key file, create a new one.\n    KeyPair userKeyPair = loadOrCreateUserKeyPair();\n\n    // Create a session.\n    Session session = new Session(CA_URI);\n\n    // Get the Account.\n    // If there is no account yet, create a new one.\n    Account acct = findOrRegisterAccount(session, userKeyPair);\n\n    // Load or create a key pair for the domains.\n    // This should not be the userKeyPair!\n    KeyPair domainKeyPair = loadOrCreateDomainKeyPair();\n\n    // Order the certificate\n    Order order = acct.newOrder().domains(domains).create();\n\n    // Perform all required authorizations\n    for (Authorization auth : order.getAuthorizations()) {\n        authorize(auth);\n    }\n\n    // Wait for the order to become READY\n    order.waitUntilReady(TIMEOUT);\n\n    // Order the certificate\n    order.execute(domainKeyPair);\n\n    // Wait for the order to complete\n    Status status = order.waitForCompletion(TIMEOUT);\n    if (status != Status.VALID) {\n        LOG.error(\"Order has failed, reason: {}\", order.getError()\n                .map(Problem::toString)\n                .orElse(\"unknown\"));\n        throw new AcmeException(\"Order failed... Giving up.\");\n    }\n\n    // Get the certificate\n    Certificate certificate = order.getCertificate();\n\n    LOG.info(\"Success! The certificate for domains {} has been generated!\", domains);\n    LOG.info(\"Certificate URL: {}\", certificate.getLocation());\n\n    // Write a combined file containing the certificate and chain.\n    try (FileWriter fw = new FileWriter(DOMAIN_CHAIN_FILE)) {\n        certificate.writeCertificate(fw);\n    }\n\n    // That's all! Configure your web server to use the\n    // DOMAIN_KEY_FILE and DOMAIN_CHAIN_FILE for the\n    // requested domains.\n}\n```\n\nWhen this method returned successfully, you will find the domain key pair in a file that is named `domain.key`, and the certificate (including the full certificate path) in a file named `domain-chain.crt`.\n\nIf no account was registered with the CA yet, there will also be a new file called `user.key`, which is your account key pair.\n\n## Creating Key Pairs\n\nThere are two sets of key pairs. One is required for creating and accessing your account, the other is required for encrypting the traffic on your domain(s). Even though it is technically possible to use a common key pair for everything, you are strongly encouraged to use separate key pairs for your account and for each of your certificates.\n\nA first helper method looks for a file that is called `user.key`. It will contain the key pair that is required for accessing your account. If there is no such key pair, a new one is generated.\n\n!!! important\n    Backup this key pair in a safe place, as you will be locked out from your account if you should ever lose it! There may be no way to recover a lost key pair or regain access to your account if the key is lost.\n\n```java\nprivate KeyPair loadOrCreateUserKeyPair() throws IOException {\n    if (USER_KEY_FILE.exists()) {\n        // If there is a key file, read it\n        try (FileReader fr = new FileReader(USER_KEY_FILE)) {\n            return KeyPairUtils.readKeyPair(fr);\n        }\n\n    } else {\n        // If there is none, create a new key pair and save it\n        KeyPair userKeyPair = ACCOUNT_KEY_SUPPLIER.get();\n        try (FileWriter fw = new FileWriter(USER_KEY_FILE)) {\n            KeyPairUtils.writeKeyPair(userKeyPair, fw);\n        }\n        return userKeyPair;\n    }\n}\n```\n\nA second helper generates a new `domain.key` file unless it is already present.\n\n```java\nprivate KeyPair loadOrCreateDomainKeyPair() throws IOException {\n    if (DOMAIN_KEY_FILE.exists()) {\n        try (FileReader fr = new FileReader(DOMAIN_KEY_FILE)) {\n            return KeyPairUtils.readKeyPair(fr);\n        }\n    } else {\n        KeyPair domainKeyPair = DOMAIN_KEY_SUPPLIER.get();\n        try (FileWriter fw = new FileWriter(DOMAIN_KEY_FILE)) {\n            KeyPairUtils.writeKeyPair(domainKeyPair, fw);\n        }\n        return domainKeyPair;\n    }\n}\n```\n\n## Registering an Account\n\nIf you do not have an account set up already, you need to create one first. The following method will show a link to the terms of service and ask you to accept it.\n\nAfter that, the `AccountBuilder` will create an account using the given account `KeyPair`. It will set an email address if provided. If the CA performs External Account Binding and a KID and HMAC is provided, it is forwarded to the CA.\n\nIf your `KeyPair` has already been registered with the CA, no new account will be created, but your existing account will be used.\n\n```java\nprivate Account findOrRegisterAccount(Session session, KeyPair accountKey) throws AcmeException {\n    // Ask the user to accept the TOS, if server provides us with a link.\n    Optional<URI> tos = session.getMetadata().getTermsOfService();\n    if (tos.isPresent()) {\n        acceptAgreement(tos.get());\n    }\n\n    AccountBuilder accountBuilder = new AccountBuilder()\n            .agreeToTermsOfService()\n            .useKeyPair(accountKey);\n\n    // Set your email (if available)\n    if (ACCOUNT_EMAIL != null) {\n        accountBuilder.addEmail(ACCOUNT_EMAIL);\n    }\n\n    // Use the KID and HMAC if the CA uses External Account Binding\n    if (EAB_KID != null && EAB_HMAC != null) {\n        accountBuilder.withKeyIdentifier(EAB_KID, EAB_HMAC);\n    }\n\n    Account account = accountBuilder.create(session);\n    LOG.info(\"Registered a new user, URL: {}\", account.getLocation());\n\n    return account;\n}\n```\n\n!!! note\n    The invocation of `agreeToTermsOfService()` is mandatory for creating a new account. Do not just invoke this method, but make sure that the user has actually read and accepted the terms of service.\n\n## Authorizing a Domain\n\nIn order to get a certificate, you need to prove ownership of the domains. In this example client, this can be done either by providing a certain file via HTTP, or by setting a certain `TXT` record in your DNS. You can choose the desired challenge type by setting the `CHALLENGE_TYPE` constant. By default, the HTTP challenge is used.\n\n```java\nprivate void authorize(Authorization auth)\n        throws AcmeException, InterruptedException {\n    LOG.info(\"Authorization for domain {}\", auth.getIdentifier().getDomain());\n\n    // The authorization is already valid.\n    // No need to process a challenge.\n    if (auth.getStatus() == Status.VALID) {\n        return;\n    }\n\n    // Find the desired challenge and prepare it.\n    Challenge challenge = null;\n    switch (CHALLENGE_TYPE) {\n        case HTTP:\n            challenge = httpChallenge(auth);\n            break;\n\n        case DNS:\n            challenge = dnsChallenge(auth);\n            break;\n    }\n\n    if (challenge == null) {\n        throw new AcmeException(\"No challenge found\");\n    }\n\n    // If the challenge is already verified,\n    // there's no need to execute it again.\n    if (challenge.getStatus() == Status.VALID) {\n        return;\n    }\n\n    // Now trigger the challenge.\n    challenge.trigger();\n\n    // Poll for the challenge to complete.\n    Status status = challenge.waitForCompletion(TIMEOUT);\n    if (status != Status.VALID) {\n        LOG.error(\"Challenge has failed, reason: {}\", challenge.getError()\n                .map(Problem::toString)\n                .orElse(\"unknown\"));\n        throw new AcmeException(\"Challenge failed... Giving up.\");\n    }\n\n    LOG.info(\"Challenge has been completed. Remember to remove the validation resource.\");\n    completeChallenge(\"Challenge has been completed.\\nYou can remove the resource again now.\");\n}\n```\n\n## HTTP Challenge\n\nFor the HTTP challenge, your server must provide a certain file in the `/.well-known/acme-challenge/` path. This file must be accessible via GET request to your domain. The request is always performed against port 80, but the CA will follow HTTP redirects. If there is a redirection to HTTPS, an invalid (e.g. self-signed, mismatched, or expired) certificate will be accepted by the CA so that the challenge can be completed.\n\nIn this example, a modal dialog will describe the file name and file content that needs to be used for the challenge. You have to manually set up your web server, so it will provide the file on the specified path. After that, confirm the dialog to trigger the challenge.\n\nWhen the authorization process is completed, the file is not used any more and can be safely deleted.\n\n```java\npublic Challenge httpChallenge(Authorization auth) throws AcmeException {\n    // Find a single http-01 challenge\n    Http01Challenge challenge = auth.findChallenge(Http01Challenge.class)\n            .orElseThrow(() -> new AcmeException(\"Found no \" + Http01Challenge.TYPE\n                    + \" challenge, don't know what to do...\"));\n\n    // Output the challenge, wait for acknowledge...\n    LOG.info(\"Please create a file in your web server's base directory.\");\n    LOG.info(\"It must be reachable at: http://{}/.well-known/acme-challenge/{}\",\n            auth.getIdentifier().getDomain(), challenge.getToken());\n    LOG.info(\"File name: {}\", challenge.getToken());\n    LOG.info(\"Content: {}\", challenge.getAuthorization());\n    LOG.info(\"The file must not contain any leading or trailing whitespaces or line breaks!\");\n    LOG.info(\"If you're ready, dismiss the dialog...\");\n\n    StringBuilder message = new StringBuilder();\n    message.append(\"Please create a file in your web server's base directory.\\n\\n\");\n    message.append(\"http://\")\n            .append(auth.getIdentifier().getDomain())\n            .append(\"/.well-known/acme-challenge/\")\n            .append(challenge.getToken())\n            .append(\"\\n\\n\");\n    message.append(\"Content:\\n\\n\");\n    message.append(challenge.getAuthorization());\n    acceptChallenge(message.toString());\n\n    return challenge;\n}\n```\n\nThis is the default challenge of the example, and probably also the most commonly used challenge. However, the CA may also offer other challenges, like the DNS challenge.\n\n## DNS Challenge\n\nFor this challenge, a `TXT` record with a given token needs to be created for the domain to be validated.\n\nAgain, a modal dialog will describe the name and content of the `TXT` record. You have to manually configure your DNS server accordingly. After that, confirm the dialog to trigger the challenge.\n\nWhen the authorization has been completed, the `TXT` record can be safely removed again.\n\n```java\npublic Challenge dnsChallenge(Authorization auth) throws AcmeException {\n    // Find a single dns-01 challenge\n    Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE)\n                .map(Dns01Challenge.class::cast)\n                .orElseThrow(() -> new AcmeException(\"Found no \" + Dns01Challenge.TYPE\n                        + \" challenge, don't know what to do...\"));\n\n    // Output the challenge, wait for acknowledge...\n    LOG.info(\"Please create a TXT record:\");\n    LOG.info(\"{} IN TXT {}\",\n            challenge.getRRName(auth.getIdentifier()), challenge.getDigest());\n    LOG.info(\"If you're ready, dismiss the dialog...\");\n\n    StringBuilder message = new StringBuilder();\n    message.append(\"Please create a TXT record:\\n\\n\");\n    message.append(challenge.getRRName(auth.getIdentifier()))\n            .append(\" IN TXT \")\n            .append(challenge.getDigest());\n    acceptChallenge(message.toString());\n\n    return challenge;\n}\n```\n\n!!! note\n    Make sure that the `TXT` record is actually available before confirming the dialog. The CA may verify the challenge immediately after it was triggered. The challenge will then fail if your DNS server was not ready yet. Depending on your hosting provider, a DNS update may take several minutes until completed.\n\n!!! note\n    For security reasons, the DNS challenge is mandatory for creating wildcard certificates. This is a restriction of the CA, and not imposed by _acme4j_.\n\n## Checking the Status\n\nThe ACME protocol does not specify the sending of events. For this reason, resource status changes must be actively polled by the client.\n\n_acme4j_ offers very simple polling methods called `waitForStatus()`, `waitUntilReady()`, and `waitForCompletion()`. These methods check the status in a synchronous busy loop. It updates the local copy of the resource using the `fetch()` method, and then checks if the status is either `VALID` or `INVALID` (or `READY` on `waitUntilReady()`). If none of these states have been reached, it just sleeps for a certain amount of time, and then rechecks the resource status.\n\nSome CAs respond with a `Retry-After` HTTP header, which provides a recommendation when to check for a status change again. If this header is present, the `fetch()` method will return the given instant. If this header is not present, we will just wait a reasonable amount of time before checking again.\n\nAn enterprise level implementation would do an asynchronous polling instead, by storing the recheck time in a database or a queue with scheduled delivery.\n\n!!! note\n    Some CAs might provide a `Retry-After` even if the resource has reached a terminal state. For this reason, always check the status _before_ waiting for the recommended time, and leave the loop if a terminal status has been reached.\n\n## User Interaction\n\nIn order to keep the example simple, Swing `JOptionPane` dialogs are used for user communication. If the user rejects a dialog, an exception is thrown and the example client is aborted.\n\n```java\npublic void acceptChallenge(String message) throws AcmeException {\n    int option = JOptionPane.showConfirmDialog(null,\n            message,\n            \"Prepare Challenge\",\n            JOptionPane.OK_CANCEL_OPTION);\n    if (option == JOptionPane.CANCEL_OPTION) {\n        throw new AcmeException(\"User cancelled the challenge\");\n    }\n}\n\npublic void completeChallenge(String message) {\n    JOptionPane.showMessageDialog(null,\n            message,\n            \"Complete Challenge\",\n            JOptionPane.INFORMATION_MESSAGE);\n}\n\npublic void acceptAgreement(URI agreement) throws AcmeException {\n    int option = JOptionPane.showConfirmDialog(null,\n            \"Do you accept the Terms of Service?\\n\\n\" + agreement,\n            \"Accept ToS\",\n            JOptionPane.YES_NO_OPTION);\n    if (option == JOptionPane.NO_OPTION) {\n        throw new AcmeException(\"User did not accept Terms of Service\");\n    }\n}\n```\n"
  },
  {
    "path": "src/doc/docs/faq.md",
    "content": "# FAQ and Troubleshooting\n\n## I have lost my account key pair. What can I do?\n\nThere is no automatic way to recover the key pair or restore access to your account.\n\nIf you just create a new account with a new key pair, subsequent domain authorization attempts are likely to fail because there is already such an authorization associated with your old account.\n\nAll you can do is to contact the CA support hotline and ask for support.\n\nYou can still revoke certificates without account key pair though, see [here](usage/revocation.md#without-account-key).\n\n## I suddenly get \"Network error\" when connecting to Pebble.\n\n**Symptom:** When connecting to Pebble, `org.shredzone.acme4j.exception.AcmeNetworkException: Network error` exceptions are thrown, with `PKIX path building failed` mentioned as cause. The code has worked before.\n\n**Cause:** Starting with v2.9.0, Pebble uses a new self-signed certificate for its API connection. It doesn't match the certificate stored in _acme4j_ before v4.0.0.\n\n**Solution:** Update to _acme4j_ v4.0.0 or higher.\n\nIf you cannot update _acme4j_, download Pebble's certificate from [here](https://raw.githubusercontent.com/letsencrypt/pebble/refs/heads/main/test/certs/pebble.minica.key.pem). Store the file either as resource (e.g. `src/main/resources` or `src/test/resources`, respectively), or as `META-INF`. It will [override the stored certificate](ca/pebble.md#custom-ca-certificate). (This workaround only affects the Pebble provider, and is safe for production use.)\n\n## My `Challenge` is in status `PENDING`. What does it mean?\n\n**Symptom:** After the challenge was triggered, it changes to status `PENDING`.\n\n**Cause:** You have triggered the challenge, and are now waiting for the CA to verify it.\n\n**Solution:** Wait until the challenge changes to either `VALID` or `INVALID` state. Do not remove challenge related resources (e.g. HTML files or DNS records) before.\n\n## My `Challenge` returns status `INVALID`. What has gone wrong?\n\n**Symptom:** After the challenge was triggered, it eventually changes to status `INVALID`.\n\n**Cause:** There may be multiple causes for that, but usually it means that the CA could not verify your challenge.\n\n**Solution:** If the status is `INVALID`, invoke `Challenge.getError()` to get the cause of the failure. For example, you can log the output of `challenge.getError().toString()`. Make sure that your challenge is ready for verification _before_ you invoke `Challenge.trigger()`. Also make sure not to remove the challenge until the status is either `VALID` or `INVALID`. Read more about errors [here](usage/errors.md).\n\n## My `Order` returns status `INVALID`. What has gone wrong?\n\n**Symptom:** Your challenge(s) passed as `VALID`. However, when you execute the order, it changes to status `INVALID`. No certificate was issued.\n\n**Cause:** There may be multiple reasons for that. It seems that you are still missing steps that are required by the CA before completion.\n\n**Solution:** If the status is `INVALID`, invoke `Order.getError()` to get the cause of the failure. For example, you can log the output of `order.getError().toString()`. Also see [here](usage/errors.md).\n\n## My `Order` seems to be stuck in status `PROCESSING`. What can I do?\n\n**Symptom:** Your challenge(s) passed as `VALID`. However, when you execute the order, it seems to be stuck in status `PROCESSING`.\n\n**Cause:** The CA may have retained your order to carry out background checks. These checks can take hours or even days. Please read the CA documentation for further details.\n\n**Solution:** There is nothing you can do on software side.\n\n## Browsers do not accept my certificate.\n\n**Symptom:** A certificate was successfully issued. However, the browser does not accept the certificate, and shows an error that the cert authority is invalid.\n\n**Cause:** This problem occurs when the staging server of a CA is used (e.g. `acme://letsencrypt.org/staging`). The certificate is signed correctly, but the staging issuer certificate is not known to the browser.\n\n**Solution:** Use the production server of your CA (e.g. `acme://letsencrypt.org`).\n\n## The http-01 challenge fails.\n\n**Symptom:** You set up your response token in the `/.well-known/acme-challenge/` path, and you can also successfully fetch it locally, but the challenge is failing with `Invalid response: 404` (or another HTTP error code).\n\n**Cause:** The CA could not access your response token, but gets a 404 page (or some other kind of error page) instead. Bear in mind that the response token is not evaluated locally by _acme4j_, but is fetched by the CA server.\n\n**Solution:** The CA server could not access your response token from the outside. One reason may be that a firewall or reverse proxy is blocking the access. Another reason may be that your local DNS resolves the domain differently. The CA uses public DNS servers to resolve the domain name. This error often happens when you try to validate a foreign domain (e.g. `example.com` or `example.org`).\n\n## `Account.getOrders()` fails with an exception.\n\n**Symptom:** According to RFC 8555, it should be possible to get a list of all orders of an account. But when I invoke `Account.getOrders()`, an `AcmeProtocolException` is thrown.\n\n**Cause:** _Let's Encrypt_ does not support getting a list of orders, even though it is mandatory by RFC 8555 (see [this issue](https://github.com/letsencrypt/boulder/issues/3335)).\n\n**Solution:** There is no solution. You need to store the location of your orders locally, and use `Login.bindOrder()` to rebind the location to your session and restore the `Order` object.\n\n## The S/MIME certificate challenge fails.\n\n**Symptom:** You try to order an S/MIME certificate from a providing CA. However, the CA constantly refuses the response e-mail because the contained ACME response is purportedly invalid.\n\n**Cause:** Unfortunately [RFC 8823](https://tools.ietf.org/html/rfc8823) has an ambiguous specification about how to concatenate the two token parts. The text permits two different interpretations that may give different results. _acme4j_ uses an implementation that corresponds to the [intention of the author of RFC 8823](https://mailarchive.ietf.org/arch/msg/acme/KusfZm3qC50IfcAAuTXtmbFK0KM/). If the CA is implemented following the other interpretation, the ACME e-mail response will not match (see [this issue](https://codeberg.org/shred/acme4j/issues/123)).\n\n**Solution:** It is a difficult situation that is caused by an ambiguous specification, but it is like it is now. Since _acme4j_ follows the intention of the RFC author, I take the position that the _acme4j_ implementation is correct. Please open a bug report at the CA, and refer to [this issue](https://codeberg.org/shred/acme4j/issues/123). If the two tokens are split at a position, so the first token won't have trailing base64 padding bits, the CA service can be implemented in a way that is compatible to both interpretations.\n\n## Suddenly acme4j starts throwing `AcmeUserActionRequiredException` everywhere! How can I fix that?\n\n**Symptom:** Many _acme4j_ methods suddenly throw a `AcmeUserActionRequiredException` after interacting with the server. It is impossible to order certificates.\n\n**Cause:** The CA has probably changed its terms of service and wants you to accept them before resuming.\n\n**Solution:** Invoke `AcmeUserActionRequiredException.getInstance()` to get a URL of a web page that describes all further steps to be taken. You might also be able to resolve the issue by logging into your CA's account, but that is up to the CA's discretion.\n\nUnfortunately, manual action is required in any case, there is no way to automate this process. This is an intentional protocol decision, and _acme4j_ is just the messenger.\n\n## Can you support us in complying with the EU Cyber Resilience Act (CRA)?\n\nNo. I'm not legally required to do so.\n\n_acme4j_ is developed and maintained by one person in their free time, with no company or organization behind it. It is non-commercial, not monetized, and there is no intent to make profit.\n\nIn other words: This software is a gift. If you use it in a commercial context, _you_ may have obligations under the CRA, but _I_ don't.\n\nPlease understand that while I welcome open collaboration and feedback, I cannot provide legal compliance support for third-party commercial use.\n\n## Development seems to be very slow. Why is that?\n\nThere are two main reasons:\n\n1. The code base is mature and stable. First released in December 2015, it has grown into a well-tested and reliable piece of software. With high unit test coverage and regular integration tests against a Pebble server, known issues have been fixed quickly over the years. That's why updates are rare: there's simply not much to do. Which means you get to enjoy a stable, dependable library.\n\n2. It's a volunteer effort. This project is maintained by a single person in their free time, with no company or funding behind it. Development happens when there's time and motivation. But after a decade of work, other priorities sometimes come first. While that means fewer updates, it also means the project isn't driven by deadlines or commercial pressure.\n\n## Where can I find the _acme4j_ Repository?\n\n* The main repository is hosted at [Codeberg](https://codeberg.org/shred/acme4j).\n* There is a fully functional mirror at [GitHub](https://github.com/shred/acme4j).\n\nYou can use both sites for posting issues and pull requests. However, Codeberg is the preferred repository.\n\n## Where can I find more help?\n\n* [Let's Encrypt Documentation](https://letsencrypt.org/docs/)\n* [Let's Encrypt Community](https://community.letsencrypt.org/) - If the question is _acme4j_ related, please mention it in your post. I don't read the forum every day, but I will answer as soon as I notice it.\n* [SSL.com Knowledge base](https://www.ssl.com/info/)\n"
  },
  {
    "path": "src/doc/docs/index.md",
    "content": "# acme4j\n\nA Java client for the _Automatic Certificate Management Environment_ (ACME) protocol as specified in [RFC 8555](https://tools.ietf.org/html/rfc8555).\n\nACME is a protocol that a certificate authority (CA) and an applicant can use to automate the process of verification and certificate issuance.\n\nThis Java client helps to connect to an ACME server, and performing all necessary steps to manage certificates.\n\nThe source code can be found at [Codeberg](https://codeberg.org/shred/acme4j) and is distributed under the terms of [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).\n\nLatest version: ![maven central](https://shredzone.org/maven-central/org.shredzone.acme4j/acme4j/badge.svg)\n\n## Features\n\n* Mature and stable code base. First release was in December 2015!\n* Fully [RFC 8555](https://tools.ietf.org/html/rfc8555) compliant\n* Supports the `http-01`, `dns-01`, and `tls-alpn-01` ([RFC 8737](https://tools.ietf.org/html/rfc8737)) challenges\n* Supports [RFC 8738](https://tools.ietf.org/html/rfc8738) IP identifier validation\n* Supports [RFC 8739](https://tools.ietf.org/html/rfc8739) short-term automatic certificate renewal (experimental)\n* Supports [RFC 8823](https://tools.ietf.org/html/rfc8823) for S/MIME certificates (experimental)\n* Supports [RFC 9444](https://tools.ietf.org/html/rfc9444) for subdomain validation\n* Supports [RFC 9773](https://tools.ietf.org/html/rfc9773) for renewal information\n* Supports [draft-ietf-acme-profiles-01](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/) for certificate profiles (experimental)\n* Supports [draft-ietf-acme-dns-account-label-02](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-account-label/) for DNS labeled with ACME account ID challenges (experimental)\n* Supports [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/) for dns-persist-01 challenges (experimental)\n* Easy to use Java API\n* Requires JRE 17 or higher\n* Supports [Actalis](https://www.actalis.com/), [Google Trust Services](https://pki.goog/), [Let's Encrypt](https://letsencrypt.org/), [SSL.com](https://www.ssl.com/), [ZeroSSL](https://zerossl.com/), and **all other CAs that comply with the ACME protocol (RFC 8555)**. Note that _acme4j_ is an independent project that is not supported or endorsed by any of the CAs.\n* Built with maven, packages available at [Maven Central](http://search.maven.org/#search|ga|1|g%3A%22org.shredzone.acme4j%22)\n* Extensive unit and integration tests\n* Adheres to [Semantic Versioning](https://semver.org/)\n\n## Dependencies\n\n* [Bouncy Castle](https://www.bouncycastle.org/)\n* [jose4j](https://bitbucket.org/b_c/jose4j/wiki/Home)\n* [slf4j](http://www.slf4j.org/)\n* For `acme4j-smime`: [Jakarta Mail](https://eclipse-ee4j.github.io/mail/), [Bouncy Castle](https://www.bouncycastle.org/)\n\n## Quick Start\n\nThere is an [example source code](example.md) included in this project. It gives an example of how to get a TLS certificate with _acme4j_.\n\n## Modules\n\n_acme4j_ consists of four modules. All modules are [available at Maven Central](https://mvnrepository.com/artifact/org.shredzone.acme4j) and can easily be added to the dependency list of your project. You can also download the jar files [at Codeberg](https://codeberg.org/shred/acme4j/releases/latest).\n\nacme4j-client\n:   [`acme4j-client`](https://mvnrepository.com/artifact/org.shredzone.acme4j/acme4j-client/latest) is the main module. It contains everything that is required to get certificates for domains.\n\n    The Java module name is `org.shredzone.acme4j`.\n\nacme4j-smime\n:   [`acme4j-smime`](https://mvnrepository.com/artifact/org.shredzone.acme4j/acme4j-smime/latest) contains the [RFC 8823](https://tools.ietf.org/html/rfc8823) implementation for ordering S/MIME certificates. It requires [Bouncy Castle](https://www.bouncycastle.org/java.html) and a `jakarta.mail` implementation.\n\n    The Java module name is `org.shredzone.acme4j.smime`.\n\nacme4j-example\n:   This module only contains [an example code](example.md) that demonstrates how to get a certificate with _acme4j_. It is not useful as a dependency in other projects.\n\nacme4j-it\n:   [`acme4j-it`](https://mvnrepository.com/artifact/org.shredzone.acme4j/acme4j-it/latest) mainly serves as integration test suite for _acme4j_ itself. It is not really useful as a dependency in other projects. However, if you write own integration tests using [pebble](https://github.com/letsencrypt/pebble) and [pebble-challtestsrv](https://hub.docker.com/r/letsencrypt/pebble-challtestsrv), you may find the [`challtestsrv` configuration client](acme4j-it/apidocs/org.shredzone.acme4j.it/org/shredzone/acme4j/it/BammBammClient.html) useful in your project.\n\n    The Java module name is `org.shredzone.acme4j.it`.\n\n## Announcements\n\nFollow our Mastodon feed for release notes and other acme4j related news.\n\n* Mastodon: <a href=\"https://foojay.social/@acme4j\" rel=\"me\">@acme4j@foojay.social</a>\n* RSS: https://foojay.social/@acme4j.rss\n\n## Contribute\n\n* Fork the [Source code at Codeberg](https://codeberg.org/shred/acme4j) or [GitHub](https://github.com/shred/acme4j). Feel free to send pull requests.\n* Found a bug? [File a bug report!](https://codeberg.org/shred/acme4j/issues) ([GitHub](https://github.com/shred/acme4j/issues))\n\n## License\n\n_acme4j_ is open source software. The source code is distributed under the terms of [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).\n"
  },
  {
    "path": "src/doc/docs/migration.md",
    "content": "# Migration Guide\n\nThis document will help you migrate your code to the latest _acme4j_ version.\n\n## Migration to Version 5.0.0\n\n- `Login.getKeyPair()` has been removed. This is a security precaution. You can access the login's public key with the new method `Login.getPublicKey()`. Private keys that were passed in as parameters cannot be accessed with the _acme4j_ API anymore.\n- `Login.getAccountLocation()` has been removed. Use `Login.getAccount().getLocation()` instead.\n\n## Migration to Version 4.0.0\n\n- Removed all methods that were marked as deprecated.\n- _acme4j_ requires JRE 17 or higher now.\n- In order to keep the API consistent, the static method `Dns01Challenge.toRRName()` is replaced with a class method `Dns01Challenge.getRRName()`. So all you have to do is to invoke `challenge.getRRName()` instead of `Dns01Challenge.toRRName()`.\n- Default network timeout has been increased from 10 seconds to 30 seconds. If you require short timeouts, you can change the duration in the [network settings](usage/advanced.md#network-settings).\n- `Session` can now be shared between multiple threads.\n- [**Buypass terminates the issuance of GoSSL certificates.**](https://community.buypass.com/t/y4y130p) Starting October 15, 2025, no new certificates will be issued. On April 15, 2026, their ACME services will be terminated. For this reason, Buypass support has been completely removed from _acme4j_. If you use _acme4j_ for Buypass services (e.g. for revocation), use their directory URL instead of the `acme://buypass.com` URI when opening a session.\n\n## Migration to Version 3.5.0\n\n- If you use STAR auto-renewal certificates, you can now use `Order.getCertificate()` instead of `Order.getAutoRenewalCertificate()` to retrieve the STAR certificate. `Order.getAutoRenewalCertificate()` is marked as deprecated, but still functional. The new method `Order.isAutoRenewalCertificate()` can be used to check if the order resulted in a standard or auto-renewing certificate.\n\n## Migration to Version 3.4.0\n\n- To be future-proof, you should wait for your `Order` resource's state to become `READY` before invoking `Order.execute()`. Most CAs change to the `READY` state immediately, but this behavior is not specified in RFC8555. Future CA implementations may stay in `PENDING` state for a short while, and would return an error if `execute()` is invoked too early. Also see the [example](example.md#the-main-workflow) for how wait for the `READY` state.\n- There are new methods `waitForCompletion()` and `waitUntilReady()` that will do the synchronous busy wait for the resource state for you. It will remove a lot of boilerplate code that is also bug prone if implemented individually. If you use synchronous polling and waiting (like shown in the example code), I recommend to change to these methods instead of waiting for the correct state yourself. See the [example](example.md) for how to use the new methods.\n- Marked `update()` (and `AcmeRetryAfterException`) as deprecated now. Please use `fetch()` instead, it returns the retry-after time as `Optional` instead of throwing an `AcmeRetryAfterException`.\n\n## Migration to Version 3.3.0\n\n- This version is unable to deserialize resource objects that were serialized by a previous version using Java's serialization mechanism. This shouldn't be a problem, as [it was not allowed](usage/persistence.md#serialization) to share serialized data between different versions anyway.\n- _acme4j_ version 2 is now discontinued. Please migrate your code to version 3. For most clients, it is less work than it seems. 😉\n\n## Migration to Version 3.2.0\n\n- Starting with this version, the `CSRBuilder` won't add the first domain as common name automatically. This permits the issuance of very long domain names, and should have no negative impact otherwise, as this field is usually ignored by CAs anyway. If you should encounter a problem here, you can use `CSRBuilder.setCommonName()` to set the first domain as common name manually. Discussion see [here](https://community.letsencrypt.org/t/questions-re-simplifying-issuance-for-very-long-domain-names/207925/11).\n- Instead of invoking `update()` and then handling an `AcmeRetryAfterException`, you should now prefer to invoke `fetch()`. It gives an optional retry-after `Instant` as return value, which makes the retry-after handling less complex. In a future version, `update()` will be fully replaced by `fetch()`, and `AcmeRetryAfterException` will be removed.\n- acme4j was updated to support the latest [draft-ietf-acme-ari-03](https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html) now. It is a breaking change! If you use ARI, make sure your server supports the latest draft before updating to this version of acme4j.\n- `Certificate.markAsReplace()` has been removed, because this method is not supported by [draft-ietf-acme-ari-03](https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html) anymore. To mark an existing certificate as replaced, use the new method `OrderBuilder.replaces()` now.\n- `Certificate.getCertID()` is not needed in the ACME context anymore. This method has been marked as deprecated. In a future version of acme4j, it will be removed without replacement. If you need the certificate ID, refer to the source code to see how it is computed, and add a similar method to your own project.\n\n## Migration to Version 3.0.0\n\nAlthough acme4j has made a major version bump, the migration of your code should be done in a few minutes for most of you.\n\n- The `acme4j-utils` module has been removed, and its classes moved into `acme4j-client` module. If you have used it before, just remove the dependency. If your project has a `module-info.java` file, remember to remove the `requires org.shredzone.acme4j.utils` line there as well.\n- All `@Nullable` return values have been removed where possible. Returned collections may now be empty, but are never `null`. Most of the other return values are now either `Optional`, or are throwing an exception if more reasonable. If your code fails to compile because the return type has changed to `Optional`, you could simply add `.orElse(null)` to emulate the old behavior. But often your code will reveal a better way to handle the former `null` pointer instead.\n- `acme4j-client` now depends on Bouncy Castle, so you might need to register it as security provider at the start of your code: `Security.addProvider(new BouncyCastleProvider())`.\n\nWhat you might also need to know:\n\n- A new `AcmeNotSupportedException` is thrown if a feature is not supported by the server. It is a subclass of the `AcmeProtocolException` runtime exception.\n- Starting with _acme4j_ v3, we will require the smallest Java SE LTS version that is still receiving premier support according to the [Oracle Java SE Support Roadmap](https://www.oracle.com/java/technologies/java-se-support-roadmap.html). At the time of writing, these are Java 11 and Java 17, so _acme4j_ requires Java 11 starting from now. With the prospected release of Java 21 (LTS) in September 2023, we will move to Java 17, and so on. If you still need Java 8, you can use _acme4j_ v2. However, it won't receive updates anymore, except of security related fixes.\n- _acme4j_ now uses the new `java.net.http` client. Due to limitations of the API, HTTP errors are only thrown with the error code, but the respective error message is missing. If you checked the error message in your unit tests, be prepared that they might fail now.\n- acme4j now accepts HTTP gzip compression. It is enabled by default, but if it causes problems or impedes debugging, it can be disabled in the `NetworkSettings` or by setting the `org.shredzone.acme4j.gzip_compression` system property to `false`.\n- All deprecated methods have been removed.\n\n## Migration to Version 2.16\n\n- In `acme4j-smime`, the `EmailProcessor.smimeMessage()` method is now deprecated. Use either `EmailProcessor.signedMessage()`, or `EmailProcessor.builder()` if you need custom verification configuration (e.g. an own trust store).\n- In `acme4j-smime`, major parts of the S/MIME message verification have been rewritten. The verification is much stricter now, and also supports secured headers in the certificate. Verification might now fail while it was successful in v2.15. Also, exception messages might have changed.\n\n## Migration to Version 2.15\n\n- `acme4j-smime` requires BouncyCastle now. The `BouncyCastleProvider` must also be added as security provider.\n- In `acme4j-smime`, the `EmailProcessor` constructor is private now. Use `EmailProcessor.plainMessage()` as drop-in replacement.\n\n## Migration to Version 2.13\n\n- The `acme4j-smime` module has switched from _JavaMail_ to _Jakarta Mail_. Unfortunately, this is a breaking API change because classes like `javax.mail.internet.InternetAddress` have moved to respective `jakarta.mail` packages.\n\n  I am aware that this change is going to cause a lot of headache, especially if your project still uses JavaEE instead of JakartaEE. However, JavaEE has been discontinued by Oracle, so all projects are going to have to do this migration sooner or later. Let's just get it over with.\n\n## Migration to Version 2.10\n\n- acme4j now provides real `module-info.java` definitions. It also means that for _building_ this project, Java 9 is the minimum requirement now.\n\n- In a preparation for Java 9 modules, the JSR305 null-safe annotations have been replaced by SpotBugs annotations. This _should_ have no impact on your code, as the method signatures themselves are unchanged. However, the compiler could now complain about some `null` dereferences that have been undetected before. Reason is that JSR305 uses the `javax.annotations` package, which leads to split packages in a Java 9 modular environment.\n\n- When fetching the directory, acme4j now evaluates HTTP caching headers instead of just caching the directory for 1 hour. However, Let's Encrypt explicitly forbids caching, which means that a fresh copy of the directory is now fetched from the server every time it is needed. I don't like it, but it is the RFC compliant behavior. It needs to be [fixed on Let's Encrypt side](https://github.com/letsencrypt/boulder/issues/4814).\n\n- `AcmeProvider.directory(Session, URI)` is now responsible for maintaining the cache. Implementations can use `Session.setDirectoryExpires()`, `Session.setDirectoryLastModified()`, and the respective getters, for keeping track of the local directory state. `AcmeProvider.directory(Session, URI)` may now return `null`, to indicate that the remote directory was unchanged, and the local copy is still valid. It's not permitted to return `null` if `Session.hasDirectory()` returns `false`, though! If your `AcmeProvider` is derived from `AbstractAcmeProvider`, and you haven't overridden the `directory()` method, no migration is necessary.\n\n## Migration to Version 2.9\n\n- In the ACME STAR draft 09, the term \"recurring\" has been changed to \"auto-renewal\". To reflect this change, all STAR related methods in the acme4j API have been renamed as well. If you are using the STAR extension, you are going to get a number of compile errors, but you will always find a corresponding new method. No functionality has been removed. I decided to do a hard API change because acme4j's STAR support is still experimental.\n\n## Migration to Version 2.8\n\n- Challenges can now be found by their class type instead of a type string, which makes finding a challenge type safe. I recommend migrating your code to this new way. The classic way is not deprecated and will not be removed though. Example:\n\n```java\nHttp01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);   // old style: by name\nHttp01Challenge challenge = auth.findChallenge(Http01Challenge.class);  // new style: by type\n```\n\n- `Login.bindChallenge()` was documented, but missing. It is available now. If you used a custom solution to bind challenges, you can now use the official way.\n\n## Migration to Version 2.7\n\n- Note that _acme4j_ has an `Automatic-Module-Name` set in the acme-client and acme-utils modules now. If you have added _acme4j_ to your Java 9+ module dependencies, you'll need to fix your dependency declaration to `org.shredzone.acme4j` (acme-client) and `org.shredzone.acme4j.utils` (acme-utils).\n\n- There are no breaking API changes in this version, except of the removal of `CertificateUtils.createTlsAlpn01Certificate(KeyPair, String, byte[])` which has been marked as deprecated in v2.6.\n\n- The ACME draft has been finalized and is now called [RFC 8555](https://tools.ietf.org/html/rfc8555). For this reason, the _acme4j_ API is now stable. There won't be breaking changes to the public API in the future, unless absolutely necessary.\n\n## Migration to Version 2.6\n\n- If you use the `tls-alpn-01` challenge and `CertificateUtils.createTlsAlpn01Certificate()` for generating its test certificate, you need to pass the domain name as an `Identifier` object instead of a `String` now. You can use `Identifier.dns(subject)` for conversion. You can also use `Authorization.getIdentifier()` to get the `Identifier` object immediately.\n\n## Migration to Version 2.5\n\n- The GET compatibility mode has been removed. It also means that the `postasget=false` parameter is ignored from now on. If you need it to connect to your ACME server, do not update to this version until your ACME server has been fixed to support ACME draft 15.\n\n!!! warning\n    _acme4j_ before version 2.5 will not work with providers like Let's Encrypt anymore!\n\n## Migration to Version 2.4\n\n- There was a major change in ACME draft 15. If you use _acme4j_ in a common way, it will transparently take care of everything in the background, so you won't even notice the change.\n\n  However, if you connect to a different ACME server than Boulder (Let's Encrypt) or Pebble, you may now get strange errors from the server if it does not support the `POST-as-GET` requests of draft 15 yet. In that case, you can add a `postasget=false` parameter to the ACME server URI (e.g. `\"https://localhost:15000/dir?postasget=false\"`). Note that this is only a temporary workaround. It will be removed in a future version. Ask the server's CA to add support for ACME draft 15.\n\n- The `AcmeProvider.connect()` method now gets the ACME server URI as parameter. It allows adding query parameters to the server URI that change the behavior of the resulting connection. If you have implemented your own AcmeProvider, just change the method's signature to `Connection connect(URI serverUri)`, and ignore the parameter value.\n\n## Migration to Version 2.3\n\n- `Authorization.getDomain()`, `Order.getDomains()` and `Problem.getDomain()` are deprecated now, and will be removed in version 2.4. If you use these methods, use `getIdentifier()` (or `getIdentifiers()`) to get an `Identifier` object, then invoke `Identifier.getDomain()` to get the domain name.\n\n## Migration to Version 2.2\n\n- No migration steps are necessary.\n\n## Migration to Version 2.1\n\n- This version adds [JSR 305](https://jcp.org/en/jsr/detail?id=305) annotations. If you use a null-safe language like Kotlin, or tools like SpotBugs, your code may fail to compile because of detected possible null pointer dereferences and unclosed streams. These are potential bugs that need to be resolved.\n\n- In _acme4j_'s `JSON` class, all `as...()` getters now expect a value to be present. For optional values, use `JSON.Value.optional()` or `JSON.Value.map()`. This class is rarely used outside _acme4j_ itself, so you usually won't need to change anything.\n\n## Migration to Version 2.0\n\n_acme4j_ 2.0 fully supports the ACMEv2 protocol. Sadly, the ACMEv2 protocol is a major change.\n\nThere is no easy recipe to migrate your code to _acme4j_ 2.0. I recommend taking a look at the example, and read this documentation. Altogether, it shouldn't be too much work to update your code, though.\n"
  },
  {
    "path": "src/doc/docs/usage/account.md",
    "content": "# Account and Login\n\nIf it is the first time you interact with the CA, you will need to register an account first.\n\nYour account requires a key pair. The public key is used to identify your account, while the private key is used to sign the requests to the ACME server.\n\n!!! note\n    The private key is never transmitted to the ACME server.\n\nYou can use external tools like `openssl` or standard Java methods to create the key pair. A more convenient way is to use the `KeyPairUtils` class. This call generates an RSA key pair with a 2048-bit key:\n\n```java\nKeyPair accountKeyPair = KeyPairUtils.createKeyPair(2048);\n```\n\nYou can also create an elliptic curve key pair:\n\n```java\nKeyPair accountKeyPair = KeyPairUtils.createECKeyPair(\"secp256r1\");\n```\n\nThe key pair can be saved to a PEM file using `KeyPairUtils.writeKeyPair()`, and read back later using `KeyPairUtils.readKeyPair()`.\n\n!!! danger\n    Your key pair is the only way to access your account. **If you should lose it, you will be locked out from your account and certificates.** The API does not offer a way to recover access after a key loss. The only way is to contact the CA's support and hope for assistance. For this reason, it is strongly recommended to keep a copy of the key pair in a safe place!\n\n## Creating an Account\n\nThe `AccountBuilder` will take care for creating a new account. Instantiate a builder, optionally add some contact information, agree to the terms of service, set the key pair, then invoke `create()`:\n\n```java\nAccount account = new AccountBuilder()\n        .addContact(\"mailto:acme@example.com\")\n        .agreeToTermsOfService()\n        .useKeyPair(keyPair)\n        .create(session);\n\nURL accountLocationUrl = account.getLocation();\n```\n\n!!! note\n    Even if it is tempting to do so, you should not invoke `agreeToTermsOfService()` automatically, but let the user confirm the terms of service first. To get a link to the current terms of service, you can invoke `session.getMetadata().getTermsOfService()`.\n\nIf the account was successfully created, you will get an `Account` object in return. Invoking its `getLocation()` method will return the location URL of your account.\n\nIt is recommended to store the location URL along with your key pair. While this is not strictly necessary, it will make it much easier to log into the account later. Unlike your key pair, the location does not need security precautions. The location can [easily be recovered if lost](#login-without-account-url).\n\n## External Account Binding\n\nAt some CAs, you need to create a customer account on their website first, and associate it with your ACME account and key pair later. The CA indicates that this process is required if `session.getMetadata().isExternalAccountRequired()` returns `true`.\n\nIn this case, your CA provides you a _Key Identifier_ (or _KID_) and a _MAC Key_ (or _HMAC Key_). You can pass these credentials to the builder using the `withKeyIdentifier()` method:\n\n```java\nString kid = ... // Key Identifier\nSecretKey macKey = ... // MAC Key\n\nAccount account = new AccountBuilder()\n        .agreeToTermsOfService()\n        .withKeyIdentifier(kid, macKey)\n        .useKeyPair(keyPair)\n        .create(session);\n\nURL accountLocationUrl = account.getLocation();\n```\n\nFor your convenience, you can also pass a base64 encoded MAC Key as `String`.\n\n!!! note\n    The MAC algorithm is automatically derived from the size of the MAC key. If a different algorithm is required, it can be set using `withMacAlgorithm()`.\n\n## Login\n\nAfter creating an account, you need to log in into it. You get a `Login` object by providing your account information to the session:\n\n```java\nKeyPair accountKeyPair = ... // account's key pair\nURL accountLocationUrl = ... // account's URL\n\nLogin login = session.login(accountLocationUrl, accountKeyPair);\n```\n\nCreating a `Login` object is very cheap. You can always create and dispose them as needed. There is no need to cache or pool them.\n\n!!! tip\n    It is possible to have multiple parallel `Login`s into different accounts in a single session. This is useful if your software handles the certificates of more than one account.\n\n## Login on Creation\n\nIf it is more convenient, you can also get a ready to use `Login` object from the `AccountBuilder` when creating a new account:\n\n```java\nLogin login = new AccountBuilder()\n        .addContact(\"mailto:acme@example.com\")\n        .agreeToTermsOfService()\n        .useKeyPair(keyPair)\n        .createLogin(session);\n\nURL accountLocationUrl = login.getAccountLocation();\n```\n\n## Login without Account URL\n\nAs mentioned above, you will need your account key pair and the account URL for logging in. If you do not know the URL, you can log into your account by creating a new account with the same key pair. The CA will detect that an account with that key is already present, and return the existing one instead.\n\nTo avoid that an actual new account is created by accident, you can use the `AccountBuilder.onlyExisting()` method:\n\n```java\nLogin login = new AccountBuilder()\n        .onlyExisting()         // Do not create a new account\n        .agreeToTermsOfService()\n        .useKeyPair(keyPair)\n        .createLogin(session);\n\nURL accountLocationUrl = login.getAccountLocation();\n```\n\nIt will return a `Login` object just from your key pair, or throw an error if the key was not known to the server.\n\nRemember that there is no way to log into your account without the key pair! \n\n## Updating the Contacts\n\nAt some point, you may want to update your account. For example your contact address might have changed. To do so, invoke `Account.modify()`, perform the changes, and invoke `commit()` to make them permanent.\n\nThe following example adds another email address.\n\n```java\nAccount account = login.getAccount();\n\naccount.modify()\n      .addContact(\"mailto:acme2@example.com\")\n      .commit();\n```\n\nYou can also get the list of contacts via `getContacts()`, and modify or remove contact `URI`s there. However, some CAs do not allow removing all contacts.\n\n!!! note\n    `AccountBuilder` only accepts contact addresses when a _new account_ is created. To modify an existing account, use `Account.modify()` as described in this section. It is not possible to modify the account using the `AccountBuilder` on an existing account.\n\n## Changing the Account Key\n\nIt is recommended to change the account key from time to time, e.g. if you suspect that your key has been compromised, or if a staff member with knowledge of the key has left the company.\n\nTo change the key pair that is associated with your account, you can use the `Account.changeKey()` method:\n\n```java\nKeyPair newKeyPair = ... // new KeyPair to be used\n\nAccount account = login.getAccount();\naccount.changeKey(newKeyPair);\n```\n\nAfter a successful change, all subsequent calls related to this account must use the new key pair. The key is automatically updated on the `Login` that was bound to this `Account` instance, so it can be used further. Other existing `Login` instances to the account need to be recreated.\n\nThe old key pair can be disposed of after that. However, better keep a backup of the old key pair until the key change was proven to be successful, by making a subsequent call with the new key pair. Otherwise, you might lock yourself out from your account if the key change should have failed silently, for whatever reason.\n\n## Account Deactivation\n\nYou can deactivate your account if you don't need it anymore:\n\n```java\naccount.deactivate();\n```\n\nDepending on the CA, the related authorizations may be automatically deactivated as well. If you want to be on the safe side, you can deactivate all authorizations manually, using `Authorization.deactivate()`.\n\nThe issued certificates may still be valid until expiration or explicit revocation. If you want to make sure the certificates are invalidated as well, [revoke](revocation.md) them prior to deactivation of your account.\n\n!!! danger\n    There is no way to reactivate the account once it has been deactivated!\n"
  },
  {
    "path": "src/doc/docs/usage/advanced.md",
    "content": "# Advanced Topics\n\n## Change of TOS\n\nIf the CA changes the terms of service and requires an explicit agreement to the new terms, an `AcmeUserActionRequiredException` will be thrown. Its `getInstance()` method returns the URL of a human-readable web document that gives instructions about how to agree to the new terms of service (e.g. by clicking on a confirmation button).\n\nUnfortunately, the `AcmeUserActionRequiredException` can be thrown at any time _acme4j_ is contacting the CA, and won't go away by itself.\n\nThere is no way to automatize this process. It requires human interaction, even on a Saturday night. Note that this is a limitation of the ACME protocol, not _acme4j_.\n\n## Custom CSR\n\nUsually _acme4j_ takes the hassle of creating a simple CSR for you. If you need more control over the CSR file, you can provide a PKCS#10 CSR file, either as `PKCS10CertificationRequest` instance or as DER formatted binary. The CSR must provide exactly the domains that you had passed to the `order()`, otherwise the finalization will fail on server side.\n\nTo create a CSR, you can use command like tools like `openssl` or Java frameworks like [Bouncy Castle](http://www.bouncycastle.org/java.html).\n\nFor your convenience, there is a [`CSRBuilder`](../acme4j-client/apidocs/org.shredzone.acme4j/org/shredzone/acme4j/util/CSRBuilder.html) that simplifies the CSR generation and should be sufficient for most use cases.\n\n```java\nKeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption\n\nCSRBuilder csrb = new CSRBuilder();\ncsrb.addDomain(\"example.org\");\ncsrb.addDomain(\"www.example.org\");\ncsrb.addDomain(\"m.example.org\");\ncsrb.setOrganization(\"The Example Organization\")\ncsrb.sign(domainKeyPair);\n\ncsrb.write(new FileWriter(\"example.csr\"));  // Write to file\n\nbyte[] csr = csrb.getEncoded();  // Get a binary representation\n```\n\nThe `CSRBuilder` also accepts IP addresses and `Identifier` for generating the CSR:\n\n```java\nCSRBuilder csrb = new CSRBuilder();\ncsrb.addIP(InetAddress.getByName(\"192.0.2.2\"));\ncsrb.addIdentifier(Identifier.ip(\"192.0.2.3\"));\ncsrb.sign(domainKeyPair);\n```\n\nThe `CSRBuilder` is used internally for creating the CSR, and you can take influence on the generated CSR by using the `Order.execute(KeyPair domainKeyPair, Consumer<CSRBuilder> builderConsumer)` method.\n\n## Domain Pre-Authorization\n\nIt is possible to proactively authorize a domain, without ordering a certificate yet. This can be useful to find out what challenges are requested by the CA to authorize a domain. It may also help to speed up the ordering process, as already completed authorizations do not need to be completed again when ordering the certificate in the near future.\n\n```java\nAccount account = ... // your Account object\nString domain = ...   // Domain name to authorize\n\nAuthorization auth = account.preAuthorize(Identifier.dns(domain));\n```\n\n!!! note\n    Some CAs may not offer domain pre-authorization, `preAuthorizeDomain()` will then fail and throw an `AcmeNotSupportedException`. Some CAs may limit pre-authorization to certain domain types (e.g. non-wildcard) and throw an `AcmeServerException` otherwise.\n\nTo pre-authorize a domain for subdomain certificates as specified in [RFC 9444](https://tools.ietf.org/html/rfc9444), flag the `Identifier` accordingly using `allowSubdomainAuth()`:\n\n```java\nAccount account = ... // your Account object\nString domain = ...   // Domain name to authorize\n\nAuthorization auth = account.preAuthorize(Identifier.dns(domain).allowSubdomainAuth());\n```\n\n## Localized Error Messages\n\nBy default, _acme4j_ will send your system's default locale as `Accept-Language` header to the CA (with a fallback to any other language). If the language is supported by the CA, it will return localized error messages.\n\nTo select another language, use `Session.setLocale()`. The change will only affect that session, so you can have multiple sessions with different locale settings.\n\n## Network Settings\n\nYou can use `Session.networkSettings()` to change some network parameters for the session.\n\n* If a proxy must be used for internet connections, you can set a `ProxySelector` instance via `setProxySelector()`.\n* To change network timeouts, use `setTimeout()`. The default timeout is 30 seconds. You can either increase the timeout for poor network connections, or reduce it to fail early on network errors. The change affects connection and read timeouts.\n* If you need authentication (e.g. for the proxy), you can set an `Authenticator` via `setAuthenticator()`. Be careful here! Most code snippets I have found on the internet will send out the full proxy credentials to anyone who is asking. You should check `Authenticator.getRequestorType()` and make sure it is `RequestorType.PROXY` before sending the proxy credentials.\n* _acme4j_ accepts HTTP `gzip` compression by default. If it should impede debugging, it can be disabled via `setCompressionEnabled(false)`.\n"
  },
  {
    "path": "src/doc/docs/usage/connecting.md",
    "content": "# Session and Connection\n\nCentral part of the communication with the CA is a [`Session`](../acme4j-client/apidocs/org.shredzone.acme4j/org/shredzone/acme4j/Session.html) object. It is used to track the communication with the ACME server.\n\nThe first step is to create such a `Session` instance.\n\n## Standard URIs\n\nThe `Session` constructor expects the URI of the ACME server's _directory_, as it is documented by the CA. This is how to connect to a fictional example staging server:\n\n```java\nSession session\n    = new Session(\"https://acme-staging-v02.api.example.org/directory\");\n```\n\nThe Session now knows where to locate the service endpoints. However, no actual connection to the server is done yet. The connection to the CA is handled later by a generic provider.\n\n## ACME URIs\n\nSuch a URI is hard to remember and might even change in the future. For this reason, special ACME connection URIs should be preferred. These special ACME URIs look like this:\n\n```java\nSession session = new Session(\"acme://example.org/staging\");\n```\n\nInstead of a generic provider, this call uses a provider that is specialized to the CA.\n\n!!! note\n    <span style=\"font-size:120%\">**→ [Find the ACME Connection URI of your CA here!](../ca/index.md) ←**</span>\n\n    If your CA is not listed there, it might still provide a JAR file with a proprietary provider that you can add to the classpath.\n\n    **You can always use the standard URI (as mentioned above) to connect to any [RFC 8555](https://tools.ietf.org/html/rfc8555) compliant CA.**\n\nA staging server is meant to be used for testing purposes only. The issued certificates are functional, but as the issuer certificate is not known to browsers, it will lead to an error if the certificate is validated.\n\nTo use the production server, you only need to change the ACME URI:\n\n```java\nSession session = new Session(\"acme://example.org\");\n```\n\n## Metadata\n\nCAs can provide metadata related to their ACME server. They are evaluated with the `session.getMetadata()` method.\n\n```java\nMetadata meta = session.getMetadata();\n\nOptional<URI> tos = meta.getTermsOfService();\nOptional<URL> website = meta.getWebsite();\n```\n"
  },
  {
    "path": "src/doc/docs/usage/debugging.md",
    "content": "# Debugging\n\nThe code base of _acme4j_ is mature and in active use for many years. There are frequent automatic integration tests running against a testing server that cover the entire process of ordering a certificate. For this reason, it is rather unlikely that _acme4j_ has an undetected major bug if used in a standard environment.\n\nHowever, problems **may** occur if _acme4j_ is used in a non-standard environment. This can be (but is not limited to):\n\n- Compatibility issues when connecting to other CA implementations than [Boulder](https://github.com/letsencrypt/boulder) and [Pebble](https://github.com/letsencrypt/pebble).\n- Using other means for creating key pairs and CSRs than given in the [example](example.md).\n- Using features of _acme4j_ that are declared as experimental.\n- Running _acme4j_ on a different Java runtime environment than OpenJDK.\n- Using _acme4j_ in a complex network architecture.\n\n_acme4j_ offers extensive debug logging that logs the client-server communication and every important aspect of the workflow. If you are stuck with a strange behavior, the first thing you should do is to enable debug logging, and check if the client-server communication gives a hint about the problem.\n\n## Enable Debug Logging\n\n_acme4j_ uses the [SLF4J](https://www.slf4j.org/) framework for logging. SLF4J is a logging _facade_. The actual logging is done by other logging frameworks, and depends on your local configuration.\n\nTo enable debug logging, lower the minimum log level of the `org.shredzone.acme4j` package to `debug`.\n\nIf you use the SLF4J simple logger, which just logs to `System.err`, this can be accomplished with a simple Java command line option:\n\n```sh\njava -Dorg.slf4j.simpleLogger.log.org.shredzone.acme4j=debug ...\n```\n\nFor other logging frameworks, please check the framework documentation.\n\nAndroid does not log SLF4J output by default. To enable debug logging to logcat, you can add [Noveo Group's android-logger](https://noveogroup.github.io/android-logger/) to your app dependencies.\n\n!!! warning\n    _acme4j_'s debug log level never logs information that might compromise your account or your certificates, like private keys. What it **does** log though are public keys, resource location URLs, nonces, contact URIs, domain names, challenges (and their responses), CSRs, certificates, and other sensitive data. For this reason, it is recommended to keep debug logging disabled on production machines.\n    \n    **Do not blindly post debug logs to public bug reports or questions!**\n\nUnderstanding the debug log output requires some basic knowledge about the [RFC 8555](https://tools.ietf.org/html/rfc8555) protocol. If you need assistance with your problem, don't hesitate to [open an issue](https://codeberg.org/shred/acme4j/issues).\n\n## Example Log Output\n\nThis is an example debug log output for creating a new account. This example connects to a [Pebble](https://github.com/letsencrypt/pebble) test server instance running locally on port 14000, so the CA's base URL is `https://localhost:14000`.\n\nUsually _acme4j_ first logs the action that is taken:\n\n```text\n[main] DEBUG org.shredzone.acme4j.AccountBuilder - create\n```\n\nIf there is no cached copy of the CA's directory, it is fetched now.\n\n```text\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - GET https://localhost:14000/dir\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Cache-Control: public, max-age=0, no-cache\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Content-Length: 406\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Date: Wed, 27 Apr 2022 17:42:43 GMT\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Content-Type: application/json; charset=utf-8\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - Result JSON: {\"keyChange\":\"https://localhost:14000/rollover-account-key\",\"meta\":{\"externalAccountRequired\":false,\"termsOfService\":\"data:text/plain,Do%20what%20thou%20wilt\"},\"newAccount\":\"https://localhost:14000/sign-me-up\",\"newNonce\":\"https://localhost:14000/nonce-plz\",\"newOrder\":\"https://localhost:14000/order-plz\",\"revokeCert\":\"https://localhost:14000/revoke-cert\"}\n```\n\nYou can see that a `GET` request to the directory `https://localhost:14000/dir` was sent, and you see the `HEADER`s that were returned by the server, and the `Result JSON` that was found in the response body.\n\nIf _acme4j_ has no current nonce, it will fetch a new one from the `newNonce` endpoint found in the directory. A `HEAD` request is sufficient for that.\n\n```text\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEAD https://localhost:14000/nonce-plz\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Cache-Control: public, max-age=0, no-cache\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Replay-Nonce: Os_sBjfWzVZenwwjvLrwXA\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Date: Wed, 27 Apr 2022 17:42:43 GMT\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Link: <https://localhost:14000/dir>;rel=\"index\"\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - Replay Nonce: Os_sBjfWzVZenwwjvLrwXA\n```\n\nIn the bottom line, the `Replay Nonce` is repeated that was found in the response header.\n\nNow _acme4j_ sends a `POST` request to the `newAccount` endpoint. As `Payload`, it will consent to the terms of service, and give optional account information (like the account e-mail address if given). You can also see the `JWS Header` that was sent with the request. It contains the target URL, the *public* JWK of your new account, the nonce from above, and the key algorithm.\n\n```text\n[main] DEBUG org.shredzone.acme4j.toolbox.JoseUtils - POST https://localhost:14000/sign-me-up\n[main] DEBUG org.shredzone.acme4j.toolbox.JoseUtils -   Payload: {\"termsOfServiceAgreed\":true}\n[main] DEBUG org.shredzone.acme4j.toolbox.JoseUtils -   JWS Header: {\"url\":\"https://localhost:14000/sign-me-up\",\"jwk\":{\"kty\":\"RSA\",\"n\":\"jyTwiSJACtW_SW-aiihQS5Y5QR704zUwjhlevY0oK-y5wP7SlIc2hq2OPVRarCzjhOxZl2AQFzM5VCR7xRDcnIn9t_pl7Mgsnx9hKDS9yQ24YXzhQ4cMEVVuqwcHvXqPdWDSoCZ1ccMqiiPyBSNGQTXMPY5PBxMOR47XwOb4eNMOPqnzVio3MEtL2wphtEonP3MY6pxJJzzel04wSCRZ4n06reqwER3KwRFPnRpRxAgmSEot5IBLIT3jj-amT5sD7YoUDbPmLk23zgDBIhX88fkClilg1W-fUi1XxYZomEPGvV7OrE1yszt4YDPqKgjJT8t2JPy__1ri-8rZgSxn5Q\",\"e\":\"AQAB\"},\"nonce\":\"Os_sBjfWzVZenwwjvLrwXA\",\"alg\":\"RS256\"}\n```\n\nThis is a possible response of the server:\n\n```text\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Cache-Control: public, max-age=0, no-cache\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Replay-Nonce: mmnKF6lBuisPWhj9kkFMRA\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Content-Length: 491\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Date: Wed, 27 Apr 2022 17:42:43 GMT\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Link: <https://localhost:14000/dir>;rel=\"index\"\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Location: https://localhost:14000/my-account/1\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Content-Type: application/json; charset=utf-8\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - Replay Nonce: mmnKF6lBuisPWhj9kkFMRA\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - Location: https://localhost:14000/my-account/1\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - Result JSON: {\"status\":\"valid\",\"orders\":\"https://localhost:14000/list-orderz/1\",\"key\":{\"kty\":\"RSA\",\"n\":\"jyTwiSJACtW_SW-aiihQS5Y5QR704zUwjhlevY0oK-y5wP7SlIc2hq2OPVRarCzjhOxZl2AQFzM5VCR7xRDcnIn9t_pl7Mgsnx9hKDS9yQ24YXzhQ4cMEVVuqwcHvXqPdWDSoCZ1ccMqiiPyBSNGQTXMPY5PBxMOR47XwOb4eNMOPqnzVio3MEtL2wphtEonP3MY6pxJJzzel04wSCRZ4n06reqwER3KwRFPnRpRxAgmSEot5IBLIT3jj-amT5sD7YoUDbPmLk23zgDBIhX88fkClilg1W-fUi1XxYZomEPGvV7OrE1yszt4YDPqKgjJT8t2JPy__1ri-8rZgSxn5Q\",\"e\":\"AQAB\"}}\n```\n\nIn the `HEADER` section, you can find a new replay nonce and the location of your new account. This information is repeated in the `Replay Nonce` and `Location` lines. You can also read the response body as `Result JSON`. It contains the account `status`, further links (e.g. for ordering), and other information.\n\n## Example Error Log Output\n\nErrors are usually sent as JSON problem structure. In the next example we have tried to create a new account, but used a bad nonce.\n\nAgain, we see the `POST` request to the `newAccount` endpoint. It uses the nonce `I6rXikEqxJ0aRwu1RvspNw` in the `JWS Header`. That nonce might have already been used in a previous request and is invalid now.\n\n```text\n[main] DEBUG org.shredzone.acme4j.toolbox.JoseUtils - POST https://localhost:14000/sign-me-up\n[main] DEBUG org.shredzone.acme4j.toolbox.JoseUtils -   Payload: {\"contact\":[\"mailto:acme@example.com\"],\"termsOfServiceAgreed\":true}\n[main] DEBUG org.shredzone.acme4j.toolbox.JoseUtils -   JWS Header: {\"url\":\"https://localhost:14000/sign-me-up\",\"jwk\":{\"kty\":\"RSA\",\"n\":\"y5i_8yG9IlL8ra2UWSK12Zy-dS0BYFvu2lerAoJQmYBwtPreOXu4OoIU6ZySAsMxlu2gMLaib62DFAFckEwQP4Bu8yJ4MWdSsiPu6pEs0SAvC61e3lYyDPbSG7FMykhWg5pjbK_NJ4Ysk64DrSA4kc0vxo54YKgxZfzObr4CHBZDaJmkTVtRndI7a8mNFO9pDlfHyb3UyZZPsg3kAUbnI9n3pZatdlGrv6eonbNAREjLvplGEI0_8B08S5fDcm6MqNarxNQIXlEhGDNoYLMGi5tM6CzsfXosHz42Umcym0EXvT1VjfoZMacSDsXleSRwjgewz486LDMErZSc0aUPSQ\",\"e\":\"AQAB\"},\"nonce\":\"I6rXikEqxJ0aRwu1RvspNw\",\"alg\":\"RS256\"}\n```\n\nThe server responds with a `400 Bad Request` and an `application/problem+json` document:\n\n```text\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Cache-Control: public, max-age=0, no-cache\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Replay-Nonce: LDDZAGcBuKYpuNlFTCxPYw\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Content-Length: 147\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Date: Wed, 27 Apr 2022 17:42:43 GMT\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Link: <https://localhost:14000/dir>;rel=\"index\"\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Content-Type: application/problem+json; charset=utf-8\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - Replay Nonce: LDDZAGcBuKYpuNlFTCxPYw\n[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - Result JSON: {\"type\":\"urn:ietf:params:acme:error:badNonce\",\"detail\":\"JWS has an invalid anti-replay nonce: I6rXikEqxJ0aRwu1RvspNw\",\"status\":400}\n```\n\nIn the `Result JSON`, you can see a JSON problem document. The `type` and `detail` fields gives further information about the error.\n\nFortunately, bad nonces are handled by _acme4j_ internally. It will just resend the request with a new nonce.\n\n```text\n[main] INFO org.shredzone.acme4j.connector.DefaultConnection - Bad Replay Nonce, trying again (attempt 1/10)\n```\n"
  },
  {
    "path": "src/doc/docs/usage/errors.md",
    "content": "# Errors\n\nThe CA will send all errors as [RFC7807](https://datatracker.ietf.org/doc/html/rfc7807) problem documents. _acme4j_ parses these documents and provides a `Problem` representation of them.\n\nThe simplest way to handle the problem is to log the result of its `.toString()` method. It contains a summary of all important fields.\n\nThere are other methods that return machine- and human-readable details and subproblems. With `.asJSON()` you can also get a JSON representation of the full problem document, in case there are non-standard fields.\n\n## Errors while Ordering\n\nIf your challenge has failed, you can retrieve the cause of the failure with `Challenge.getError()` or `Order.getError()`. It returns an `Optional` containing further details why the challenge has failed.\n\n## Exceptions\n\n`AcmeServerException` and its subclasses provide a `getProblem()` method that returns the `Problem` that caused the exception. The exception message also contains a summary of the problem.\n"
  },
  {
    "path": "src/doc/docs/usage/exceptions.md",
    "content": "# Exceptions\n\n_acme4j_ methods can throw a number of exceptions. All exceptions are derived from `AcmeException` and are checked exceptions. Only `AcmeLazyLoadingException` and `AcmeProtocolException` are runtime exceptions.\n\n```text\nException\n├ AcmeException\n│ ├ AcmeNetworkException\n│ ├ AcmeRetryAfterException\n│ └ AcmeServerException\n│   ├ AcmeRateLimitedException\n│   ├ AcmeUnauthorizedException\n│   └ AcmeUserActionRequiredException\n└ RuntimeException\n  ├ AcmeLazyLoadingException\n  └ AcmeProtocolException\n    └ AcmeNotSupportedException\n```\n\n## AcmeException\n\nThis is the root class of all checked _acme4j_ exceptions.\n\n## AcmeNetworkException\n\nThis is an `AcmeException` that is thrown on generic network errors during communication (e.g. network timeout).\n\nThe exception provides the causing `IOException`.\n\n## AcmeRetryAfterException\n\nThis `AcmeException` shows that a server-side process has not been completed yet, and gives an estimation when the process might be completed.\n\nIt can only be thrown when invoking `update()`. However, it is preferred to invoke `fetch()`, which returns the retry-after instant directly, instead of throwing this exception.\n\n!!! note\n    The internal state of the resource is still updated.\n\nThe given estimation is only a proposal. This exception can be safely ignored. However, an earlier attempt to update the state will likely throw this exception again.\n\n## AcmeServerException\n\nAn `AcmeException` that is thrown when the server responded with an error. The cause of the error is returned as `Problem` object.\n\nA few special cases are throwing a subclass exception, which is easier to handle.\n\n## AcmeRateLimitedException\n\nThis `AcmeServerException` shows that the client has exceeded a rate limit of the server, and the request was denied because of that.\n\nThe exception provides a `Problem` instance that further explains what rate limit has been exceeded. Optionally it also provides an `Instant` when the request is expected to succeed again. It also provides `URL`s to human-readable documents with further information about the rate limit.\n\n## AcmeUnauthorizedException\n\nAn `AcmeServerException` that indicates that the client has insufficient permissions for the attempted request. For example, this exception is thrown when an account tries to access a resource that belongs to a different account.\n\n## AcmeUserActionRequiredException\n\nThis `AcmeServerException` is thrown when a user action is required. The most likely reason is that the Terms of Service have been changed and must be confirmed before proceeding.\n\nThe exception provides a `Problem` object with a detailed reason, a link to a web page with further instructions to be taken by a human, and an optional link to the new Terms of Service.\n\n## AcmeLazyLoadingException\n\nThis is a runtime exception that is thrown when an `AcmeException` occurs while a resource lazily tries to update its current state from the server.\n\nAfter construction, all [resources](persistence.md) do not hold the state of the resource yet. For this reason, it is cheap to construct resources, as it does not involve network traffic.\n\nTo fetch the current state of the resource, `update()` can be invoked. In case of an error, the `update()` method throws a checked `AcmeException`.\n\nAll getter methods of a resource invoke `update()` implicitly if the current state is unknown. However, it would make usage much more complex if every getter could throw the checked `AcmeException`. For this reason, the getters wrap the `AcmeException` into a runtime `AcmeLazyLoadingException`.\n\nIf you want to avoid this exception to be thrown, you can invoke `update()` on the resource, and handle the `AcmeException` there in a single place. After that, the getters won't throw an `AcmeLazyLoadingException` anymore.\n\nThe exception returns the resource type, the resource location, and the `AcmeException` that was thrown.\n\n## AcmeProtocolException\n\nThis is a runtime exception that is thrown if the server response was unexpected and violates the RFC. _acme4j_ was unable to parse the response properly.\n\nAn example would be that the server returned an invalid JSON structure that could not be parsed.\n\n## AcmeNotSupportedException\n\nThis is an `AcmeProtocolException` that is thrown if the server does not support the requested feature. This can be because the feature is optional, or because the server is not fully RFC compliant.\n\nThe exception provides a description of the feature that was missing.\n"
  },
  {
    "path": "src/doc/docs/usage/index.md",
    "content": "# How to Use acme4j\n\n_acme4j_ is a client library that helps to connect to ACME servers without worrying about specification details.\n\nThis is the main part of the documentation about how to use _acme4j_.\n\n* [Session and Connection](connecting.md): How to connect to an ACME server\n* [Account and Login](account.md): How to create an account and login\n* [Certificate Ordering](order.md): How to order a certificate for your domain\n* [Certificate Renewal](renewal.md): How to renew a certificate in time before expiration\n* [Certificate Revocation](revocation.md): How to revoke a certificate\n* [Resources and Persistence](persistence.md): How to persist ACME resources\n* [Exceptions](exceptions.md): About all _acme4j_ exceptions\n* [Advanced Topics](advanced.md): Advanced topics about _acme4j_ and ACME\n* [Debugging](debugging.md): Enabling and reading the debug log output\n\nThe first three chapters are essential, as they describe all necessary steps for getting a signed certificate.\n"
  },
  {
    "path": "src/doc/docs/usage/order.md",
    "content": "# Certificate Ordering\n\nOnce you have your account set up, you are ready to order certificates.\n\n## Creating an Order\n\nUse `Account.newOrder()` to start ordering a new certificate. It returns an `OrderBuilder` object that helps you to collect the parameters of the order. You can give one or more domain names. Optionally you can also give your desired `notBefore` and `notAfter` dates for the generated certificate, but it is at the discretion of the CA to use (or ignore) these values.\n\n```java\nAccount account = ... // your Account object\n\nOrder order = account.newOrder()\n        .domains(\"example.org\", \"www.example.org\", \"m.example.org\")\n        .notAfter(Instant.now().plus(Duration.ofDays(20L)))     // optional\n        .create();\n```\n\n!!! note\n    The number of domains per certificate may be limited. See your CA's documentation for the limits.\n\n## Authorization\n\nThe `Order` resource contains a collection of `Authorization` objects that can be read from the `getAuthorizations()` method.\n\nEach `Authorization` is associated with one of the domains in your order. `Authorization.getIdentifier()` returns that identifier. Before you can retrieve your certificate, you must process _all_ authorizations that are in a `PENDING` state.\n\n```java\nfor (Authorization auth : order.getAuthorizations()) {\n  if (auth.getStatus() == Status.PENDING) {\n    log.info(\"Authorizing \" + auth.getIdentifier());\n\n    // process auth by performing a challenge, see below\n     :\n     :\n  }\n}\n```\n\nIf all `Authorization` objects are in status `VALID`, you are ready to [finalize your order](#finalizing-the-order).\n\n## Challenge\n\nThe `Authorization` instance contains further details about how you can prove the ownership of your domain. An ACME server offers one or more authorization methods, called `Challenge`.\n\n`Authorization.getChallenges()` returns a collection of all `Challenge`s offered by the CA for domain ownership validation. You only need to complete _one_ of them to successfully authorize your domain. You would usually pick the challenge that is best suited for your infrastructure.\n\n!!! tip\n    See [here](../challenge/index.md) for a description of all standard challenges. However, your CA may not offer all the standard types, and may offer additional, proprietary challenge types.\n\nThe simplest way is to invoke `findChallenge()`, stating the challenge type your system is able to provide (either as challenge name or challenge class type):\n\n```java\nOptional<Http01Challenge> challenge = auth.findChallenge(Http01Challenge.TYPE); // by name\nOptional<Http01Challenge> challenge = auth.findChallenge(Http01Challenge.class); // by type\n```\n\nIt returns a properly cast `Challenge` object, or _empty_ if your challenge type was not offered by the CA. In this example, your system chooses `Http01Challenge` because it is able to respond to an [http-01](../challenge/http-01.md) challenge.\n\n!!! tip\n    Passing the challenge type is preferred over the challenge name, as type checks are performed at compile time here. Passing in the challenge name might result in a `ClassCastException` at runtime.\n\nThe returned `Challenge` resource provides all the data that is necessary for a successful verification of your domain ownership (see the documentation of the individual challenges).\n\nAfter you have performed the necessary steps to set up the response to the challenge (e.g. configuring your web server or modifying your DNS records), you tell the ACME server that you are ready for validation:\n\n```java\nchallenge.trigger();\n```\n\nNow you have to wait for the server to check your response. If the checks are completed, the CA will set the authorization status to `VALID` or `INVALID`. The easiest (but admittedly also the ugliest) way is to poll the status:\n\n```java\nwhile (!EnumSet.of(Status.VALID, Status.INVALID).contains(auth.getStatus())) {\n  Thread.sleep(3000L);\n  auth.fetch();\n}\n```\n\nThis is a very simple example which can be improved in many ways:\n\n* Limit the number of checks, to avoid endless loops if an authorization is stuck on server side.\n* Wait with the status checks until the CA has accessed the response for the first time (e.g. after an incoming HTTP request to the response file).\n* Use an asynchronous architecture instead of a blocking `Thread.sleep()`.\n* Check if `auth.fetch()` returns a retry-after `Instant`, and wait for the next update at least until this moment is reached. See the [example](../example.md) for a simple way to do that.\n\nThe CA server may start with the validation immediately after `trigger()` is invoked, so make sure your server is ready to respond to requests before invoking `trigger()`. Otherwise, the challenge might fail instantly.\n\nAlso keep your response available until the status has changed to `VALID` or `INVALID`. The ACME server may check your response multiple times, and from different IPs! If the status gets `VALID` or `INVALID`, the response you have set up before is not needed anymore. It can (and should) be removed.\n\n!!! tip\n    A common mistake is that the server infrastructure is not completely ready when `trigger()` is invoked (e.g. caches are not purged, services are still restarting, synchronization between instances is still in progress). Also, do not tear down the challenge response too early, as the CA might perform multiple checks.\n\nIf your authorization status turned to `VALID`, you have successfully authorized your domain, and you are ready for the next step.\n\n## Finalizing the Order\n\nAfter successfully completing all authorizations, the order needs to be finalized.\n\nFirst, you will need to generate a key pair that is used for certification and encryption of the domain. Similar to the account key pair, you can either use external tool, Java's own crypto framework, or use the [`KeyPairUtils`](../acme4j-client/apidocs/org.shredzone.acme4j.utils/org/shredzone/acme4j/util/KeyPairUtils.html).\n\n!!! tip\n    Never use your account key pair as domain key pair, but always generate separate key pairs!\n\nAfter that, the order can be finalized:\n\n```java\nKeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption\n\norder.execute(domainKeyPair);\n```\n\n_acme4j_ will automatically take care of creating a minimal CSR for this order internally. If you need to expand this CSR (e.g. with your company name), you can do so:\n\n```java\norder.execute(domainKeyPair, csr -> {\n    csr.setOrganization(\"ACME Corp.\");\n});\n```\n\nIt depends on the CA if other CSR properties (like _Organization_, _Organization Unit_) are accepted. Some may even require these properties to be set, while others may ignore them when generating the certificate.\n\nYou can also create a custom CSR, and pass it to the order with either `execute(PKCS10CertificationRequest csr)` or `execute(byte[] csr)`.\n\n!!! note\n    Some CAs may take a considerable amount of time (30 seconds or more) for finalizing an order. As this call is synchronous, be prepared that the process is blocked for that time. If you experience frequent timeouts with your CA, you can increase the timeout duration in the [network settings](advanced.md#network-settings).\n\n!!! note\n    According to RFC-8555, the correct technical term is _finalization_ of an order. However, Java has a method called `Object.finalize()` which is problematic and should not be used. To avoid confusion with that method, the finalization methods are intentionally called `execute` in _acme4j_.\n\n## Retrieving the Certificate\n\nOnce you completed all the previous steps, it is finally time to download the signed certificate.\n\nBut first we need to wait until the certificate is available for download. Again, a primitive way is to poll the status:\n\n```java\nOrder order = ... // your Order object from the previous step\n\nwhile (!EnumSet.of(Status.VALID, Status.INVALID).contains(order.getStatus())) {\n  Thread.sleep(3000L);\n  order.fetch();\n}\n```\n\nThis is a very simple example which can be improved in many ways:\n\n* Limit the number of checks, to avoid endless loops if the order is stuck on server side.\n* Use an asynchronous architecture instead of a blocking `Thread.sleep()`.\n* Check if `order.fetch()` returns a retry-after `Instant`, and wait for the next update at least until this moment is reached. See the [example](../example.md) for a simple way to do that.\n\n!!! tip\n    If the status is `PENDING`, you have not completed all authorizations yet.\n\n!!! note\n    Always check the status before downloading the certificate, even if it seems that the CA sets the status to `VALID` immediately.\n\nAs soon as the status turns `VALID`, you can retrieve a `Certificate` object:\n\n```java\nCertificate cert = order.getCertificate();\n```\n\nThe `Certificate` object offers methods to get the certificate or the certificate chain.\n\n```java\nX509Certificate cert = cert.getCertificate();\nList<X509Certificate> chain = cert.getCertificateChain();\n```\n\n`cert` only contains your leaf certificate. However, most servers require the certificate `chain` that also contains all intermediate certificates up to the root CA.\n\nYou can write the certificate chain to disk using the `Certificate.writeCertificate()` method. It will create a `.crt` file that is accepted by most servers (like _Apache_, _nginx_, _postfix_, _dovecot_, etc.).\n\n**Congratulations! You have just created your first certificate via _acme4j_.**\n\n## List all Orders\n\nTo get a list of all current orders of your account, invoke `Account.getOrders()`.\n\nNote that for reasons lying in the ACME protocol, the result is an `Iterator<Order>` and not a list. Also, any invocation of `Iterator.next()` can initiate a network call to the CA, and may throw an `AcmeProtocolException` if there was an error.\n\n!!! important\n    This method is a mandatory part of RFC-8555. Still, as of now, this functionality has not been implemented in all [Boulder](https://github.com/letsencrypt/boulder) based CAs (like Let's Encrypt) and will throw an `AcmeNotSupportedException`. Also see [this issue](https://github.com/letsencrypt/boulder/issues/3335). At the moment, the only workaround is to store `Order` location URLs (or other resource URLs) locally along with the certificates, see the [Resources and Persistence](./persistence.md) chapter.\n\n\n## Wildcard Certificates\n\nIf supported by the CA, you can also generate a wildcard certificate that is valid for all subdomains of a domain, by prefixing the domain name with `*.` (e.g. `*.example.org`). The domain itself is not covered by the wildcard certificate and also needs to be added to the order if necessary.\n\n!!! note\n    _acme4j_ accepts all kind of wildcard notations (e.g. `www.*.example.org`, `*.*.example.org`). However, those notations are not specified. They may be rejected by the CA, or may not work as expected.\n\nYou must be able to prove ownership of the domain that you want to order a wildcard certificate for (i.e. for `*.example.org` ownership of `example.org` needs to be proven). The corresponding `Authorization` resource only refers to that domain, and does not contain the wildcard notation. However, the `Authorization.isWildcard()` method will reveal that this authorization is related to a wildcard certificate.\n\nThe following example creates an `Order` for `example.org` and `*.example.org`:\n\n```java\nKeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption\n\nOrder order = account.newOrder()\n        .domains(\"example.org\", \"*.example.org\")\n        .create();\n\norder.execute(domainKeyPair);\n```\n\nIn the subsequent authorization process, you would only have to prove ownership of the `example.org` domain.\n\n!!! note\n    Some CAs may reject wildcard certificate orders at all, may only offer a limited set of challenge types, or may require special challenge types that are not documented here. Refer to your CA's documentation to find out about the wildcard certificate policy.\n\n## IP Identifiers\n\nBesides domains, _acme4j_ also supports IP identifier validation as specified in [RFC 8738](https://tools.ietf.org/html/rfc8738). If your CA offers ACME IP support, you can add IP `Identifier` objects to the order:\n\n```java\nOrder order = account.newOrder()\n        .identifier(Identifier.ip(InetAddress.getByName(\"192.0.2.2\")))\n        .identifier(Identifier.ip(\"192.0.2.3\"))   // for your convenience\n        .identifier(Identifier.dns(\"example.org\"))\n        .create();\n```\n\nThe example also shows how to add domain names as DNS `Identifier` objects. Adding domain names via `domain()` is just a shortcut notation for it.\n\n## Subdomains\n\nOrdering certificates for subdomains is not different to ordering certificates for domains. You prove ownership of that subdomain, and then get a certificate for it.\n\nIf your CA supports [RFC 9444](https://tools.ietf.org/html/rfc9444), you can also get certificates for all subdomains only by proving ownership of an ancestor domain. To do so, add the ancestor domain to your `Identifier` when creating the order:\n\n```java\nOrder order = account.newOrder()\n        .identifier(\n            Identifier.dns(\"foo.bar.example.org\")\n                .withAncestorDomain(\"example.org\")\n        )\n        .create();\n```\n\nThe CA can then choose to issue challenges for any of `foo.bar.example.org`, `bar.example.org`, or `example.org`. For each challenge, the related domain can be got via `Authorization.getIdentifier()`.\n\n`Authorization.isSubdomainAuthAllowed()` will return `true` if that `Authorization` is used to issue subdomain certificates.\n\nTo check if your CA supports RFC 9444, read `Metadata.isSubdomainAuthAllowed()`.\n\n## Profiles\n\nIf your CA supports [draft-ietf-acme-profiles-01](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/), you can select a profile when ordering a certificate:\n\n```java\nOrder order = account.newOrder()\n        .profile(\"tlsserver\")\n        .create();\n```\n\nYou can use `Metadata` to check if profiles are supported, and which ones:\n\n* `Metadata.isProfileAllowed()`: `true` if profiles are supported\n* `Metadata.isProfileAllowed(String)`: `true` if the given profile is supported\n* `Metadata.getProfiles()`: returns a `Set` of all profile names\n* `Metadata.getProfileDescription(String)`: returns a human-readable profile description\n"
  },
  {
    "path": "src/doc/docs/usage/persistence.md",
    "content": "# Resources and Persistence\n\nAll CA related resources are derived from the `AcmeResource` class:\n\n* `Account`: Represents your account\n* `Authorization`: Authorization of a domain or identifier\n* `Certificate`: A certificate\n* `Challenge` (and subclasses): Challenge to proof domain ownership\n* `Order`: A certificate order\n* `RenewalInfo`: Renewal information\n\nThese classes reflect the state of the corresponding resource on the ACME server side. They also keep a copy of the current resource state that can be updated via `update()`. The only exception is the `Certificate` resource, which will never change its state and thus does not need to be updated.\n\n## Resource Location\n\nAll resources possess a unique resource URL on the CA server. To get that URL, invoke the `getLocation()` method. This is the best way to retrieve a permanent resource reference for local persistence (e.g. in a database).\n\n## Resource Binding\n\nTo revive an `AcmeResource` object from its location URL, you can bind it to your `Login` by using the resource location URL and the corresponding method:\n\n* `Login.bindAuthorization()` takes an authorization URL and returns the corresponding `Authorization` object.\n* `Login.bindCertificate()` takes a certificate URL and returns the corresponding `Certificate` object.\n* `Login.bindOrder()` takes an order URL and returns the corresponding `Order` object.\n* `Login.bindRenewalInfo()` takes a renewal info URL and returns the corresponding `RenewalInfo` object.\n\nThere are two methods for binding a `Challenge`:\n\n* `Login.bindChallenge(URL location)` binds to a challenge URL and returns a `Challenge` instance. You will need to check yourself if the `Challenge` is of the expected type, and eventually cast it to the correct type.\n* `bindChallenge(URL location, Class type)` is similar to the method above, but will do the casting for you. You will get the challenge object in your expected type. If the challenge at the location is of a different type, an `AcmeProtocolException` will be thrown.\n\nThere is no way to bind an `Account`. To retrieve your account resource, simply invoke `Login.getAccount()`.\n\n!!! note\n    You can only bind resources that belong to your account.\n\n## Serialization\n\nAll resource objects are serializable, so the current state of the object can also be frozen by Java's serialization mechanism.\n\nHowever, the `Login` that the object is bound to is _not_ serialized! This is because in addition to volatile data, the `Login` object also holds a copy of your private key. Not serializing it prevents you from accidentally exposing your private key in a place with lowered access restrictions.\n\nAfter deserialization, an object is not bound to a `Login` yet. It is required to rebind it by invoking the `rebind()` method of the resource object.\n\n!!! note\n    Serialization is only meant for short term storage at runtime, not for long term persistence. For long term persistence, always store the location URL of the resource, then bind it at later time like mentioned above.\n\n!!! warning\n    Do not share serialized data between different versions of _acme4j_.\n"
  },
  {
    "path": "src/doc/docs/usage/renewal.md",
    "content": "# Certificate Renewal\n\nCertificates are only valid for a limited time, and need to be renewed before expiry.\n\nTo read the expiration date of your certificate, use `X509Certificate.getNotAfter()`. The certificate is eligible to be renewed a few days or weeks before its expiry. Check the documentation of your CA about a recommended time window. Also do not postpone the renewal to the last minute, as there can always be unexpected network issues that delay the issuance of a renewed certificate.\n\n!!! tip\n    Some CAs send a notification mail to your account's mail addresses in time before expiration. However, you should not rely on those mails, and only use them as an ultimate warning.\n\n## How to Renew\n\nThere is no special path for renewing a certificate. To renew it, just [order](order.md) the certificate again.\n\n## Renewal Information\n\n_acme4j_ supports [RFC 9773](https://tools.ietf.org/html/rfc9773) for renewal information.\n\nYou can check if the CA offers renewal information by invoking `Certificate.hasRenewalInfo()`. If it does, you can get a suggested time window for certificate renewal by invoking `Certificate.getRenewalInfo()`.\n\nWhen renewing a certificate, you can use `OrderBuilder.replaces()` to mark your current certificate as the one being replaced. This step is optional though.\n\n## Short-Term Automatic Renewal\n\n_acme4j_ supports [RFC 8739](https://tools.ietf.org/html/rfc8739) for Short-Term Automatic Renewal (STAR) of certificates.\n\nTo find out if the CA supports the STAR extension, check the metadata:\n\n```java\nif (session.getMetadata().isAutoRenewalEnabled()) {\n  // CA supports STAR!\n}\n```\n\nIf STAR is supported, you can enable automatic renewals by adding `autoRenewal()` to the order parameters:\n\n```java\nOrder order = account.newOrder()\n        .domain(\"example.org\")\n        .autoRenewal()\n        .create();\n```\n\nYou can also use `autoRenewalStart()`, `autoRenewalEnd()`, `autoRenewalLifetime()` and `autoRenewalLifetimeAdjust()` to change the time span and frequency of automatic renewals. You cannot use `notBefore()` and `notAfter()` in combination with `autoRenewal()` though.\n\nThe `Metadata` object also holds the accepted renewal limits (see `Metadata.getAutoRenewalMinLifetime()` and `Metadata.getAutoRenewalMaxDuration()`).\n\nThe STAR certificates are automatically renewed by the CA. You will always find the latest certificate at the certificate location URL.\n\nTo download the latest certificate issue, you can bind the certificate URL to your `Login` and then use the `Certificate` object.\n\n```java\nURL certificateUrl = ... // URL of the certificate\n\nCertificate cert = login.bindCertificate(certificateUrl);\nX509Certificate latestCertificate = cert.getCertificate();\n```\n\n!!! note\n    STAR based certificates cannot be revoked. However, as it is the nature of these certs to be very short-lived, this does not pose an actual security issue.\n\n### Fetching STAR certificates via GET\n\nUsually the STAR certificate must be fetched from the location URL by an authorized `POST-as-GET` request. If supported by the CA, it is possible to change the method to a plain `GET` request, so the certificate can be fetched by a simple HTTP client (like curl) without authentication.\n\nTo enable this `GET` method, first check if it is offered by the CA, by invoking `Metadata.isAutoRenewalGetAllowed()`. If it is true, add `autoRenewalEnableGet()` to the order options. After the order was finalized, the certificate will be available via both `GET` and `POST-as-GET` methods.\n\n### Cancelling Auto-Renewals\n\nUse `Order.cancelAutoRenewal()` to terminate automatic certificate renewals.\n"
  },
  {
    "path": "src/doc/docs/usage/revocation.md",
    "content": "# Certificate Revocation\n\nTo revoke a certificate, just invoke the respective method:\n\n```java\ncert.revoke();\n```\n\nOptionally, you can provide a revocation reason that the ACME server may use when generating OCSP responses and CRLs.\n\n```java\ncert.revoke(RevocationReason.KEY_COMPROMISE);\n```\n\nThere are different reasons for a certificate revocation. If you have sold or deleted the associated domain, you should also deactivate the respective `Authorization` using `Authorization.deactivate()`. Otherwise, the new owner of the domain might have problems to get a certificate because the domain name is still associated with your account.\n\n!!! tip\n    It is not documented if the deactivation of an authorization also revokes the related certificate automatically. If in doubt, revoke the certificate yourself before deactivation.\n\n## Without Certificate URL\n\nIf you cannot create a `Certificate` object because you don't know the certificate's location URL, you can also use an alternative method that only requires a `Login` and the certificate itself:\n\n```java\nLogin login = ...           // login to your account\nX509Certificate cert = ...  // certificate to revoke\n\nCertificate.revoke(login, cert, RevocationReason.KEY_COMPROMISE);\n```\n\n## Without Account Key\n\nIf you have lost your account key, you can still revoke a certificate as long as you still own the domain key pair that was used for the order. `Certificate` provides a special method for this case.\n\n```java\nKeyPair domainKeyPair = ... // the key pair used for order (not your account key pair)\nX509Certificate cert = ...  // certificate to revoke\n\nCertificate.revoke(session, domainKeyPair, cert, RevocationReason.KEY_COMPROMISE);\n```\n\n!!! warning\n    There is no automatized way to revoke a certificate if you have lost both your account's key pair and your domain's key pair.\n"
  },
  {
    "path": "src/doc/mkdocs.yml",
    "content": "site_name: acme4j\nsite_author: Richard Körber\nsite_url: https://acme4j.shredzone.org\nsite_dir: target/site/\nrepo_url: https://codeberg.org/shred/acme4j\nedit_uri: ''\nuse_directory_urls: false\ntheme:\n    name: readthedocs\n    custom_dir: theme/\n    highlightjs: false\nmarkdown_extensions:\n  - admonition\n  - codehilite\n  - def_list\n  - toc:\n      permalink: true\nnav:\n  - 'index.md'\n  - 'example.md'\n  - JavaDocs:\n    - 'acme4j-client': acme4j-client/apidocs/index.html\n    - 'acme4j-smime': acme4j-smime/apidocs/index.html\n    - 'acme4j-it': acme4j-it/apidocs/index.html\n  - Usage:\n    - 'usage/index.md'\n    - 'usage/connecting.md'\n    - 'usage/account.md'\n    - 'usage/order.md'\n    - 'usage/renewal.md'\n    - 'usage/revocation.md'\n    - 'usage/persistence.md'\n    - 'usage/errors.md'\n    - 'usage/exceptions.md'\n    - 'usage/advanced.md'\n    - 'usage/debugging.md'\n    - 'faq.md'\n  - Challenges:\n    - 'challenge/index.md'\n    - 'challenge/dns-01.md'\n    - 'challenge/dns-account-01.md'\n    - 'challenge/dns-persist-01.md'\n    - 'challenge/email-reply-00.md'\n    - 'challenge/http-01.md'\n    - 'challenge/tls-alpn-01.md'\n  - CA:\n    - 'ca/index.md'\n    - 'ca/actalis.md'\n    - 'ca/google.md'\n    - 'ca/letsencrypt.md'\n    - 'ca/pebble.md'\n    - 'ca/sslcom.md'\n    - 'ca/zerossl.md'\n  - Development:\n    - 'development/index.md'\n    - 'development/provider.md'\n    - 'development/challenge.md'\n    - 'development/testing.md'\n    - 'migration.md'\n"
  },
  {
    "path": "src/doc/theme/breadcrumbs.html",
    "content": ""
  },
  {
    "path": "src/doc/theme/css/font.css",
    "content": "/*\n * Lato\n * Copyright 2010-2011 tyPoland Lukasz Dziedzic\n * SIL Open Font License, 1.1\n */\n@font-face {\n  font-family: 'Lato';\n  font-style: normal;\n  font-weight: 400;\n  src: local('Lato Regular'), local('Lato-Regular'), url('../fonts/lato-v15-latin-regular.woff') format('woff');\n}\n\n/*\n * Roboto Slab\n * Copyright 2013 Google\n * Apache License, version 2.0\n */\n@font-face {\n  font-family: 'Roboto Slab';\n  font-style: normal;\n  font-weight: 400;\n  src: local('Roboto Slab Regular'), local('RobotoSlab-Regular'), url('../fonts/roboto-slab-v8-latin-regular.woff') format('woff');\n}\n\n/*\n * Inconsolata\n * Copyright 2006 The Inconsolata Project Authors\n * SIL Open Font License, 1.1\n */\n@font-face {\n  font-family: 'Inconsolata';\n  font-style: normal;\n  font-weight: 400;\n  src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('../fonts/inconsolata-v17-latin-regular.woff') format('woff');\n}\n"
  },
  {
    "path": "src/doc/theme/css/theme_custom.css",
    "content": "\n.wy-nav-content {\n    max-width: none;\n}\n\n.codehilite pre code {\n    font-size: 15px;\n}\n\n.wy-menu-vertical p.caption {\n    padding: 0 1rem;\n    font-size: 100%;\n    border-bottom: 1px solid #555;\n}\n\n.wy-menu-vertical ul {\n    margin-bottom: .7rem;\n}\n\np {\n    margin-bottom: .5rem;\n}\n\nh2 {\n    margin-top: 2rem;\n}\n\ntable {\n    margin-bottom: 2rem;\n}\n\nfooter {\n    margin-top: 2rem;\n}\n"
  },
  {
    "path": "src/doc/theme/css/theme_pygments.css",
    "content": "\n/* github pygments theme by @jwarby, https://github.com/jwarby/jekyll-pygments-themes */\n.codehilite .hll { background-color: #ffffcc }\n.codehilite .c { color: #999988; font-style: italic }\n.codehilite .err { color: #a61717; background-color: #e3d2d2 }\n.codehilite .k { color: #000000; font-weight: bold }\n.codehilite .o { color: #000000; font-weight: bold }\n.codehilite .cm { color: #999988; font-style: italic }\n.codehilite .cp { color: #999999; font-weight: bold; font-style: italic }\n.codehilite .c1 { color: #999988; font-style: italic }\n.codehilite .cs { color: #999999; font-weight: bold; font-style: italic }\n.codehilite .gd { color: #000000; background-color: #ffdddd }\n.codehilite .ge { color: #000000; font-style: italic }\n.codehilite .gr { color: #aa0000 }\n.codehilite .gh { color: #999999 }\n.codehilite .gi { color: #000000; background-color: #ddffdd }\n.codehilite .go { color: #888888 }\n.codehilite .gp { color: #555555 }\n.codehilite .gs { font-weight: bold }\n.codehilite .gu { color: #aaaaaa }\n.codehilite .gt { color: #aa0000 }\n.codehilite .kc { color: #000000; font-weight: bold }\n.codehilite .kd { color: #000000; font-weight: bold }\n.codehilite .kn { color: #000000; font-weight: bold }\n.codehilite .kp { color: #000000; font-weight: bold }\n.codehilite .kr { color: #000000; font-weight: bold }\n.codehilite .kt { color: #445588; font-weight: bold }\n.codehilite .m { color: #009999 }\n.codehilite .s { color: #d01040 }\n.codehilite .na { color: #008080 }\n.codehilite .nb { color: #0086B3 }\n.codehilite .nc { color: #445588; font-weight: bold }\n.codehilite .no { color: #008080 }\n.codehilite .nd { color: #3c5d5d; font-weight: bold }\n.codehilite .ni { color: #800080 }\n.codehilite .ne { color: #990000; font-weight: bold }\n.codehilite .nf { color: #990000; font-weight: bold }\n.codehilite .nl { color: #990000; font-weight: bold }\n.codehilite .nn { color: #555555 }\n.codehilite .nt { color: #000080 }\n.codehilite .nv { color: #008080 }\n.codehilite .ow { color: #000000; font-weight: bold }\n.codehilite .w { color: #bbbbbb }\n.codehilite .mf { color: #009999 }\n.codehilite .mh { color: #009999 }\n.codehilite .mi { color: #009999 }\n.codehilite .mo { color: #009999 }\n.codehilite .sb { color: #d01040 }\n.codehilite .sc { color: #d01040 }\n.codehilite .sd { color: #d01040 }\n.codehilite .s2 { color: #d01040 }\n.codehilite .se { color: #d01040 }\n.codehilite .sh { color: #d01040 }\n.codehilite .si { color: #d01040 }\n.codehilite .sx { color: #d01040 }\n.codehilite .sr { color: #009926 }\n.codehilite .s1 { color: #d01040 }\n.codehilite .ss { color: #990073 }\n.codehilite .bp { color: #999999 }\n.codehilite .vc { color: #008080 }\n.codehilite .vg { color: #008080 }\n.codehilite .vi { color: #008080 }\n.codehilite .il { color: #009999 }\n"
  },
  {
    "path": "src/doc/theme/footer.html",
    "content": "<footer>\n  <div class=\"rst-footer-buttons\" role=\"navigation\" aria-label=\"footer navigation\">\n    {% if page.next_page %}\n      <a href=\"{{ page.next_page.url|url }}\" class=\"btn btn-neutral float-right\" title=\"{{ page.next_page.title }}\">Next <span class=\"icon icon-circle-arrow-right\"></span></a>\n    {% endif %}\n    {% if page.previous_page %}\n      <a href=\"{{ page.previous_page.url|url }}\" class=\"btn btn-neutral\" title=\"{{ page.previous_page.title }}\"><span class=\"icon icon-circle-arrow-left\"></span> Previous</a>\n    {% endif %}\n  </div>\n</footer>\n"
  },
  {
    "path": "src/doc/theme/main.html",
    "content": "{% extends \"base.html\" %}\n\n{% block styles %}\n<link rel=\"stylesheet\" href=\"{{ 'css/font.css'|url }}\" />\n<link rel=\"stylesheet\" href=\"{{ 'css/theme.css'|url }}\" />\n<link rel=\"stylesheet\" href=\"{{ 'css/theme_extra.css'|url }}\" />\n<link rel=\"stylesheet\" href=\"{{ 'css/theme_custom.css'|url }}\" />\n<link rel=\"stylesheet\" href=\"{{ 'css/theme_pygments.css'|url }}\" />\n{%- for path in config['extra_css'] %}\n<link href=\"{{ path|url }}\" rel=\"stylesheet\" />\n{%- endfor %}\n{% endblock %}\n\n{% block repo %}\n<a href=\"https://shredzone.org/maven/acme4j/acme4j-client/apidocs/index.html\">API javadoc</a>s\n{% endblock %}\n"
  }
]