diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..b4d0e49 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,28 @@ +FROM public.ecr.aws/docker/library/docker:24-dind +RUN set -ex \ + && apk add --no-cache --update \ + zsh tmux vim curl git jq yq k9s skopeo \ + alpine-zsh-config zsh-theme-powerlevel10k \ + zsh-syntax-highlighting zsh-autosuggestions \ + sudo ttf-dejavu font-terminus font-inconsolata font-dejavu \ + font-noto font-noto-cjk font-awesome font-noto-extra \ + openjdk17-jdk py3-pip +RUN set -ex \ + && git clone https://github.com/powerline/fonts.git --depth=1 \ + && ( cd fonts; ./install.sh; ) \ + && rm -rf fonts +ARG MYUID=${MYUID:-1000} +RUN set -ex \ + && adduser -D -G root -s /bin/zsh -u $MYUID vscode \ + && addgroup vscode wheel \ + && addgroup vscode docker \ + && sed -i '/root:x:0:0:root/ s/ash/zsh/g' /etc/passwd \ + && sed -i '/# %wheel ALL.*/s/^# //' /etc/sudoers +COPY zshrc /root/.zshrc +COPY tmux.conf /root/.tmux.conf +COPY --chown=vscode zshrc /home/vscode/.zshrc +COPY --chown=vscode tmux.conf /home/vscode/.tmux.conf +VOLUME /home/vscode +VOLUME /root +ENV LANG='C.UTF-8' +ENV LANGUAGE='en_US:en' diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..aa8f989 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,13 @@ +{ + "image": "mcr.microsoft.com/devcontainers/java:17", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/common-utils:2": {}, + "ghcr.io/devcontainers/features/node:1": {} + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", + "workspaceFolder": "/workspace", + "mounts": [ + "source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/vscode/.ssh,type=bind,readonly" + ] +} diff --git a/.devcontainer/tmux.conf b/.devcontainer/tmux.conf new file mode 100644 index 0000000..57bc2d3 --- /dev/null +++ b/.devcontainer/tmux.conf @@ -0,0 +1,7 @@ +set -g default-shell /bin/zsh +set -g default-terminal "xterm-256color" +set -g status-right '#(true)' +bind-key a setw sync +set -g mouse on +bind-key -n Home send Escape "OH" +bind-key -n End send Escape "OF" diff --git a/.devcontainer/zshrc b/.devcontainer/zshrc new file mode 100644 index 0000000..d29dc1d --- /dev/null +++ b/.devcontainer/zshrc @@ -0,0 +1,24 @@ +export LANG=C.UTF-8 +export LANGUAGE=en_US:en +source /usr/share/zsh/plugins/powerlevel10k/powerlevel9k.zsh-theme +source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh +source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh +autoload -Uz compinit +compinit +export TERM="xterm-256color" +ZSH_THEME="powerlevel10k/powerlevel9k" +POWERLEVEL9K_PROMPT_ON_NEWLINE=true +POWERLEVEL9K_CONTEXT_TEMPLATE="%m" +POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(root_indicator context dir vcs) +POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=() +HISTSIZE=10000 +SAVEHIST=10000 +HISTFILE=~/.zsh_history +setopt EXTENDED_HISTORY +setopt HIST_EXPIRE_DUPS_FIRST +setopt HIST_IGNORE_DUPS +setopt HIST_IGNORE_ALL_DUPS +setopt HIST_IGNORE_SPACE +setopt HIST_FIND_NO_DUPS +setopt HIST_SAVE_NO_DUPS +setopt HIST_BEEP diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0211db3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM eclipse-temurin:17-jdk AS builder +WORKDIR /workspace +COPY . . +ARG JAR_FILE=build/libs/*.jar +ARG GRADLE_USER_HOME=/tmp/build_cache/gradle +RUN --mount=type=cache,target=/tmp/build_cache/gradle \ + set -ex \ + && chmod +x gradlew \ + && ./gradlew build -i \ + -x test -x check -x asciidoctor \ + && rm -f build/libs/*-plain.jar \ + && java -Djarmode=layertools -jar $JAR_FILE extract + +FROM eclipse-temurin:17-jre +USER 999:0 +WORKDIR /app +COPY --from=builder /workspace/dependencies/ ./ +COPY --from=builder /workspace/spring-boot-loader/ ./ +COPY --from=builder /workspace/snapshot-dependencies/ ./ +COPY --from=builder /workspace/application/ ./ +ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] diff --git a/compose_build.md b/compose_build.md new file mode 100644 index 0000000..b341199 --- /dev/null +++ b/compose_build.md @@ -0,0 +1,51 @@ +### Docker Compose Build +``` +docker-compose build --progress=plain +docker-compose up -d +docker-compose ps +docker-compose logs -f +``` +### Generate CSR +``` +openssl req -new -nodes -keyout /tmp/test.key -out /tmp/test.csr \ + -subj '/O=test/CN=test.com' \ + -addext 'basicConstraints=CA:false' \ + -addext 'keyUsage=digitalSignature,keyEncipherment' \ + -addext 'extendedKeyUsage=clientAuth,serverAuth' \ + -addext 'subjectAltName=IP:127.0.0.1,DNS:localhost' +``` +``` +openssl req -text -noout -in /tmp/test.csr +``` +### Sign CSR +``` +curl -sL "http://localhost:8080/pki/v1/certificates" \ + -H "Content-Type: application/x-pem-file" \ + -F "file=@/tmp/test.csr" \ + -o /tmp/test.crt \ + -D /tmp/test.crt_headers.txt +``` +### Veriry Certificate +``` +openssl x509 -text -noout -in /tmp/test.crt +``` +``` +openssl verify -CAfile <(curl -sL "http://localhost:8080/pki/v1/cacert") /tmp/test.crt +``` +``` +openssl verify -crl_check \ + -CAfile <(curl -sL "http://localhost:8080/pki/v1/cacert") \ + -CRLfile <(curl -sL "http://localhost:8080/pki/v1/crl") /tmp/test.crt +``` +### Revoke Certificate +``` +SN=$(grep X-Cert-Serial-Number /tmp/test.crt_headers.txt | tr -d '\r' | awk '{print $NF}') + +curl -sL -X DELETE "http://localhost:8080/pki/v1/certificates/$SN" +``` +### Veriry CRL +``` +openssl verify -crl_check \ + -CAfile <(curl -sL "http://localhost:8080/pki/v1/cacert") \ + -CRLfile <(curl -sL "http://localhost:8080/pki/v1/crl") /tmp/test.crt +``` diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 730a1ec..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,16 +0,0 @@ -version: '3.3' - -services: - db: - image: mariadb:latest - container_name: mariadb - ports: - - "3306:3306" - volumes: - - ./data/mariadb:/var/lib/mysql - restart: always - environment: - MYSQL_ROOT_PASSWORD: OtJagbee - MYSQL_DATABASE: pki - MYSQL_USER: user - MYSQL_PASSWORD: seacKoop diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..52d6f38 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,59 @@ +volumes: + postgres_data: +services: + postgres: + image: postgres:16-alpine + environment: + TZ: America/Sao_Paulo + update: 1 + POSTGRES_DB: PKI + POSTGRES_USER: pki + POSTGRES_PASSWORD: s3cret + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - postgres_data:/var/lib/postgresql/data/pgdata + healthcheck: + test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 -d $$POSTGRES_DB -U $$POSTGRES_USER"] + start_period: 5s + interval: 10s + timeout: 5s + retries: 3 + ports: + - "5432:5432" + app: + build: + context: . + dockerfile: Dockerfile + environment: + TZ: America/Sao_Paulo + server_port: 8080 + management_server_port: 8081 + management_endpoints_web_exposure_include: info,health,prometheus + management_endpoints_web_basePath: /actuator + management_endpoint_health_probes_enabled: true + management_endpoint_health_showDetails: never + management_health_defaults_enabled: false + spring.datasource.username: pki + spring.datasource.password: s3cret + spring.datasource.url: jdbc:postgresql://postgres:5432/PKI + spring.flyway.connect-retries: 10 + # volumes: + # - ./config:/app/config + healthcheck: + start_period: 30s + interval: 10s + timeout: 5s + retries: 3 + test: curl -fsSL http://127.0.0.1:8081/actuator/health + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 512M + ports: + - "8080:8080" + - "8081:8081" + depends_on: + postgres: + condition: service_healthy diff --git a/src/main/kotlin/org/nordix/simplepki/application/service/DefaultPkiOperations.kt b/src/main/kotlin/org/nordix/simplepki/application/service/DefaultPkiOperations.kt index 51cf8f5..aec3623 100644 --- a/src/main/kotlin/org/nordix/simplepki/application/service/DefaultPkiOperations.kt +++ b/src/main/kotlin/org/nordix/simplepki/application/service/DefaultPkiOperations.kt @@ -29,6 +29,9 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers +import org.bouncycastle.asn1.x509.Extensions +import org.bouncycastle.asn1.x509.GeneralNames import org.nordix.simplepki.domain.model.PkiEntity import org.nordix.simplepki.domain.model.PkiOperations import org.nordix.simplepki.domain.model.RevocationEntry @@ -64,6 +67,13 @@ internal class DefaultPkiOperations(private val clock: Clock) : PkiOperations { .addExtension(Extension.basicConstraints, true, CertificateSettings.NON_CA_BASIC_CONSTRAINTS) .addExtension(Extension.keyUsage, true, CertificateSettings.NON_CA_KEY_USAGES) .addExtension(Extension.extendedKeyUsage, true, CertificateSettings.EXTENDED_KEY_USAGES) + + // Add the Subject Alternative Name (SAN) extension if present in the CSR + val sanExtension = getSubjectAlternativeNames(csr) + if (sanExtension != null) { + certificateBuilder.addExtension(Extension.subjectAlternativeName, false, sanExtension) + } + val signer = JcaContentSignerBuilder(CertificateSettings.SIGNATURE_ALGORITHM) .build(ca.privateKey) return JcaX509CertificateConverter() @@ -84,4 +94,18 @@ internal class DefaultPkiOperations(private val clock: Clock) : PkiOperations { return JcaX509CRLConverter() .getCRL(crlHolder) } + + private fun getSubjectAlternativeNames(csr: PKCS10CertificationRequest): GeneralNames? { + val attributes = csr.attributes + for (attribute in attributes) { + if (attribute.attrType == PKCSObjectIdentifiers.pkcs_9_at_extensionRequest) { + val extensions = Extensions.getInstance(attribute.attrValues.getObjectAt(0)) + val sanExtension = extensions.getExtension(Extension.subjectAlternativeName) + if (sanExtension != null) { + return GeneralNames.getInstance(sanExtension.parsedValue) + } + } + } + return null + } }