init docker

This commit is contained in:
Anthony Stirling 2024-12-09 18:18:16 +00:00
parent a772b4fa09
commit d59cb18666
18 changed files with 507 additions and 318 deletions

View File

@ -6,7 +6,6 @@ COPY scripts /scripts
COPY pipeline /pipeline COPY pipeline /pipeline
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
#COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/ #COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
COPY build/libs/*.jar app.jar
ARG VERSION_TAG ARG VERSION_TAG
@ -19,6 +18,10 @@ ENV DOCKER_ENABLE_SECURITY=false \
PGID=1000 \ PGID=1000 \
UMASK=022 UMASK=022
# Create non-root user first
RUN addgroup -S stirlingpdfgroup && \
adduser -S stirlingpdfuser -G stirlingpdfgroup
# JDK for app # JDK for app
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
@ -31,8 +34,6 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
bash \ bash \
curl \ curl \
qpdf \ qpdf \
shadow \
su-exec \
openssl \ openssl \
openssl-dev \ openssl-dev \
openjdk21-jre \ openjdk21-jre \
@ -50,15 +51,16 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
# uno unoconv and HTML # uno unoconv and HTML
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \ pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
mv /usr/share/tessdata /usr/share/tessdata-original && \ mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
fc-cache -f -v && \ fc-cache -f -v && \
chmod +x /scripts/* && \
chmod +x /scripts/init.sh && \
# User permissions # User permissions
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ mkdir -p ${HOME} /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /scripts /usr/share/fonts/custom && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \ chown -R stirlingpdfuser:stirlingpdfgroup ${HOME} /configs /logs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \ chmod -R 755 ${HOME} /configs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
tesseract --list-langs tesseract --list-langs && \
chmod -R 777 /logs
COPY build/libs/*.jar app.jar
RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
EXPOSE 8080/tcp EXPOSE 8080/tcp

View File

@ -1,4 +1,4 @@
# Build the application # Build stage
FROM gradle:8.11-jdk17 AS build FROM gradle:8.11-jdk17 AS build
# Set the working directory # Set the working directory
@ -7,18 +7,20 @@ WORKDIR /app
# Copy the entire project to the working directory # Copy the entire project to the working directory
COPY . . COPY . .
# Build the application with DOCKER_ENABLE_SECURITY=false # Build the application
RUN DOCKER_ENABLE_SECURITY=true \ RUN DOCKER_ENABLE_SECURITY=true \
./gradlew clean build ./gradlew clean build
# Main stage # Main stage
FROM alpine:3.20.3 FROM alpine:3.20.3
# Copy necessary files # Create non-root user first
RUN addgroup -S stirlingpdfgroup && \
adduser -S stirlingpdfuser -G stirlingpdfgroup
COPY scripts /scripts COPY scripts /scripts
COPY pipeline /pipeline COPY pipeline /pipeline
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY --from=build /app/build/libs/*.jar app.jar
ARG VERSION_TAG ARG VERSION_TAG
@ -33,11 +35,15 @@ ENV DOCKER_ENABLE_SECURITY=false \
FAT_DOCKER=true \ FAT_DOCKER=true \
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
# Create necessary directories with correct permissions
RUN mkdir -p ${HOME} /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /scripts /usr/share/fonts/custom && \
chown -R stirlingpdfuser:stirlingpdfgroup ${HOME} /configs /logs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
chmod -R 755 ${HOME} /configs /logs /customFiles /pipeline /scripts /usr/share/fonts/custom
# JDK for app # JDK and other dependencies
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \ echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \
apk upgrade --no-cache -a && \ apk upgrade --no-cache -a && \
apk add --no-cache \ apk add --no-cache \
ca-certificates \ ca-certificates \
@ -45,39 +51,29 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
tini \ tini \
bash \ bash \
curl \ curl \
shadow \
su-exec \
openssl \ openssl \
openssl-dev \ openssl-dev \
openjdk21-jre \ openjdk21-jre \
# Doc conversion
libreoffice \ libreoffice \
# pdftohtml
poppler-utils \ poppler-utils \
# OCR MY PDF (unpaper for descew and other advanced featues)
qpdf \ qpdf \
tesseract-ocr-data-eng \ tesseract-ocr-data-eng \
tesseract-ocr-data-fra \
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \ font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
# CV
py3-opencv \ py3-opencv \
# python3/pip
python3 \ python3 \
py3-pip && \ py3-pip && \
# uno unoconv and HTML
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \ pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
mv /usr/share/tessdata /usr/share/tessdata-original && \ mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \ mkdir -p /usr/share/tessdata && \
fc-cache -f -v && \ chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tessdata /usr/share/fonts/opentype/noto && \
chmod +x /scripts/* && \ fc-cache -f -v
chmod +x /scripts/init.sh && \
# User permissions COPY build/libs/*.jar app.jar
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
tesseract --list-langs
EXPOSE 8080/tcp EXPOSE 8080/tcp
# Set user and run command
ENTRYPOINT ["tini", "--", "/scripts/init.sh"] ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"] CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]

View File

@ -17,7 +17,10 @@ COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
COPY scripts/installFonts.sh /scripts/installFonts.sh COPY scripts/installFonts.sh /scripts/installFonts.sh
COPY pipeline /pipeline COPY pipeline /pipeline
COPY build/libs/*.jar app.jar
# Create non-root user first
RUN addgroup -S stirlingpdfgroup && \
adduser -S stirlingpdfuser -G stirlingpdfgroup
# Set up necessary directories and permissions # Set up necessary directories and permissions
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
@ -30,18 +33,15 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
tini \ tini \
bash \ bash \
curl \ curl \
shadow \
su-exec \
openjdk21-jre && \ openjdk21-jre && \
# User permissions # User permissions
mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto && \ mkdir -p ${HOME} /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /scripts /usr/share/fonts/custom && \
chmod +x /scripts/*.sh && \ chown -R stirlingpdfuser:stirlingpdfgroup ${HOME} /configs /logs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ chmod -R 755 ${HOME} /configs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline && \ chmod -R 777 /logs
chown stirlingpdfuser:stirlingpdfgroup /app.jar
# Set environment variables COPY build/libs/*.jar app.jar
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
EXPOSE 8080/tcp EXPOSE 8080/tcp

View File

@ -14,14 +14,17 @@ services:
ports: ports:
- 8080:8080 - 8080:8080
volumes: volumes:
- /stirling/latest/data:/usr/share/tessdata:rw - ./stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw - ./stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw - ./stirling/latest/logs:/logs:rw
user: "stirlingpdfuser"
environment: environment:
DOCKER_ENABLE_SECURITY: "true" DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "false" SECURITY_ENABLELOGIN: "false"
PUID: 1002 PUID: 1002
PGID: 1002 PGID: 1002
LANGS: "ALL"
TESSERACT_LANGS: "eng,fra,deu,spa,ita"
UMASK: "022" UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF UI_APPNAME: Stirling-PDF

View File

@ -14,9 +14,9 @@ services:
ports: ports:
- "8080:8080" - "8080:8080"
volumes: volumes:
- /stirling/latest/data:/usr/share/tessdata:rw - ./stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw - ./stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw - ./stirling/latest/logs:/logs:rw
environment: environment:
DOCKER_ENABLE_SECURITY: "true" DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true" SECURITY_ENABLELOGIN: "true"

View File

@ -14,9 +14,9 @@ services:
ports: ports:
- "8080:8080" - "8080:8080"
volumes: volumes:
- /stirling/latest/data:/usr/share/tessdata:rw - ./stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw - ./stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw - ./stirling/latest/logs:/logs:rw
environment: environment:
DOCKER_ENABLE_SECURITY: "true" DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true" SECURITY_ENABLELOGIN: "true"

View File

@ -14,9 +14,9 @@ services:
ports: ports:
- "8080:8080" - "8080:8080"
volumes: volumes:
- /stirling/latest/data:/usr/share/tessdata:rw - ./stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw - ./stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw - ./stirling/latest/logs:/logs:rw
environment: environment:
DOCKER_ENABLE_SECURITY: "true" DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true" SECURITY_ENABLELOGIN: "true"

View File

@ -14,8 +14,8 @@ services:
ports: ports:
- "8080:8080" - "8080:8080"
volumes: volumes:
- /stirling/latest/config:/configs:rw - ./stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw - ./stirling/latest/logs:/logs:rw
environment: environment:
DOCKER_ENABLE_SECURITY: "false" DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false" SECURITY_ENABLELOGIN: "false"

View File

@ -14,9 +14,9 @@ services:
ports: ports:
- "8080:8080" - "8080:8080"
volumes: volumes:
- /stirling/latest/data:/usr/share/tessdata:rw - ./stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw - ./stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw - ./stirling/latest/logs:/logs:rw
environment: environment:
DOCKER_ENABLE_SECURITY: "false" DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false" SECURITY_ENABLELOGIN: "false"

View File

@ -1,21 +1,5 @@
#!/bin/bash #!/bin/bash
# Update the user and group IDs as per environment variables
if [ ! -z "$PUID" ] && [ "$PUID" != "$(id -u stirlingpdfuser)" ]; then
usermod -o -u "$PUID" stirlingpdfuser || true
fi
if [ ! -z "$PGID" ] && [ "$PGID" != "$(getent group stirlingpdfgroup | cut -d: -f3)" ]; then
groupmod -o -g "$PGID" stirlingpdfgroup || true
fi
umask "$UMASK" || true
if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" && "$FAT_DOCKER" != "true" ]]; then
echo "issue with calibre in current version, feature currently disabled on Stirling-PDF"
#apk add --no-cache calibre@testing
fi
if [[ "$FAT_DOCKER" != "true" ]]; then if [[ "$FAT_DOCKER" != "true" ]]; then
/scripts/download-security-jar.sh /scripts/download-security-jar.sh
fi fi
@ -24,14 +8,4 @@ if [[ -n "$LANGS" ]]; then
/scripts/installFonts.sh $LANGS /scripts/installFonts.sh $LANGS
fi fi
echo "Setting permissions and ownership for necessary directories..." exec "$@"
# Attempt to change ownership of directories and files
if chown -R stirlingpdfuser:stirlingpdfgroup $HOME /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /app.jar; then
chmod -R 755 /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /app.jar || true
# If chown succeeds, execute the command as stirlingpdfuser
exec su-exec stirlingpdfuser "$@"
else
# If chown fails, execute the command without changing the user context
echo "[WARN] Chown failed, running as host user"
exec "$@"
fi

View File

@ -1,27 +1,35 @@
#!/bin/bash #!/bin/bash
# Copy the original tesseract-ocr files to the volume directory without overwriting existing files # Copy the original tesseract-ocr files to the volume directory without overwriting existing files
echo "Copying original files without overwriting existing files" echo "Copying original files without overwriting existing files"
mkdir -p /usr/share/tessdata cp -rn /usr/share/tessdata-original/* /usr/share/tessdata 2>/dev/null || true
cp -rn /usr/share/tessdata-original/* /usr/share/tessdata
# Copy additional tessdata if available
if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then
cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tessdata || true; cp -rn /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tessdata 2>/dev/null || true
fi fi
if [ -d /usr/share/tesseract-ocr/5/tessdata ]; then if [ -d /usr/share/tesseract-ocr/5/tessdata ]; then
cp -r /usr/share/tesseract-ocr/5/tessdata/* /usr/share/tessdata || true; cp -rn /usr/share/tesseract-ocr/5/tessdata/* /usr/share/tessdata 2>/dev/null || true
fi fi
# Check if TESSERACT_LANGS environment variable is set and is not empty # Check if TESSERACT_LANGS environment variable is set and is not empty
if [[ -n "$TESSERACT_LANGS" ]]; then if [[ -n "$TESSERACT_LANGS" ]]; then
# Convert comma-separated values to a space-separated list # Convert comma-separated values to a space-separated list
LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ') TES_LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$' pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$'
# Install each language pack
for LANG in $LANGS; do # Log available languages
echo "Currently installed languages:"
tesseract --list-langs
echo "Requested additional languages: $TES_LANGS"
# Instead of apk add, download language files from a known source
for LANG in $TES_LANGS; do
if [[ $LANG =~ $pattern ]]; then if [[ $LANG =~ $pattern ]]; then
apk add --no-cache "tesseract-ocr-data-$LANG" # Download to user-writable directory
wget -P /usr/share/tessdata/ "https://github.com/tesseract-ocr/tessdata/raw/main/${LANG}.traineddata" || \
echo "Failed to download language pack for ${LANG}"
else else
echo "Skipping invalid language code" echo "Skipping invalid language code"
fi fi

View File

@ -1,67 +1,156 @@
#!/bin/bash #!/bin/bash
LANGS=$1 LANGS=$1
FONT_DIR="$HOME/.local/share/fonts"
TEMP_DIR=$(mktemp -d)
# Function to install a font package # Create fonts directory if it doesn't exist
install_font() { mkdir -p "$FONT_DIR"
echo "Installing font package: $1"
if ! apk add "$1" --no-cache; then # Function to get latest GitHub release
echo "Failed to install $1" get_latest_release() {
fi local repo=$1
local api_url="https://api.github.com/repos/$repo/releases/latest"
curl --silent "$api_url" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
} }
# Install common fonts used across many languages # Function to download and install a font
#common_fonts=( install_font() {
# font-terminus local font_name=$1
# font-dejavu echo "Installing font package: $font_name"
# font-noto
# font-noto-cjk
# font-awesome
# font-noto-extra
#)
#
#for font in "${common_fonts[@]}"; do
# install_font $font
#done
# Map languages to specific font packages # Map font package names to actual font URLs and installation methods
case $font_name in
"font-dejavu")
local version=$(get_latest_release "dejavu-fonts/dejavu-fonts")
version=${version#version_} # Remove 'version_' prefix
local url="https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_${version}/dejavu-fonts-ttf-${version}.tar.bz2"
wget -q "$url" -P "$TEMP_DIR" && \
tar xjf "$TEMP_DIR/dejavu-fonts-ttf-${version}.tar.bz2" -C "$TEMP_DIR" && \
find "$TEMP_DIR" -name "*.ttf" -exec cp {} "$FONT_DIR/" \;
;;
"font-noto")
# Base Noto Sans and Serif
wget -q "https://noto-website-2.storage.googleapis.com/pkgs/NotoSans-hinted.zip" -P "$TEMP_DIR" && \
wget -q "https://noto-website-2.storage.googleapis.com/pkgs/NotoSerif-hinted.zip" -P "$TEMP_DIR" && \
unzip -q "$TEMP_DIR/NotoSans-hinted.zip" -d "$TEMP_DIR/noto-sans" && \
unzip -q "$TEMP_DIR/NotoSerif-hinted.zip" -d "$TEMP_DIR/noto-serif" && \
cp "$TEMP_DIR/noto-sans"/*.ttf "$FONT_DIR/" && \
cp "$TEMP_DIR/noto-serif"/*.ttf "$FONT_DIR/"
;;
"font-noto-cjk")
# Noto CJK
wget -q "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/Japanese/NotoSansCJKjp-Regular.otf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/Korean/NotoSansCJKkr-Regular.otf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Regular.otf" -P "$FONT_DIR"
;;
"font-noto-arabic")
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoNaskhArabic/NotoNaskhArabic-Regular.ttf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoKufiArabic/NotoKufiArabic-Regular.ttf" -P "$FONT_DIR"
;;
"font-noto-devanagari")
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSansDevanagari/NotoSansDevanagari-Regular.ttf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSerifDevanagari/NotoSerifDevanagari-Regular.ttf" -P "$FONT_DIR"
;;
"font-noto-thai")
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSansThai/NotoSansThai-Regular.ttf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSerifThai/NotoSerifThai-Regular.ttf" -P "$FONT_DIR"
;;
"font-noto-hebrew")
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSansHebrew/NotoSansHebrew-Regular.ttf" -P "$FONT_DIR"
;;
"font-awesome")
local version=$(get_latest_release "FortAwesome/Font-Awesome")
wget -q "https://use.fontawesome.com/releases/v${version}/fontawesome-free-${version}-desktop.zip" -P "$TEMP_DIR" && \
unzip -q "$TEMP_DIR/fontawesome-free-${version}-desktop.zip" -d "$TEMP_DIR" && \
cp "$TEMP_DIR/fontawesome-free-${version}-desktop/otfs"/*.otf "$FONT_DIR/"
;;
"font-source-code-pro")
local version=$(get_latest_release "adobe-fonts/source-code-pro")
wget -q "https://github.com/adobe-fonts/source-code-pro/releases/download/${version}/TTF-source-code-pro-${version}.zip" -P "$TEMP_DIR" && \
unzip -q "$TEMP_DIR/TTF-source-code-pro-${version}.zip" -d "$TEMP_DIR/source-code-pro" && \
cp "$TEMP_DIR/source-code-pro"/*.ttf "$FONT_DIR/"
;;
"font-vollkorn")
wget -q "https://github.com/FAlthausen/Vollkorn-Typeface/raw/main/fonts/TTF/Vollkorn-Regular.ttf" -P "$FONT_DIR"
;;
"font-liberation")
wget -q "https://github.com/liberationfonts/liberation-fonts/files/7261482/liberation-fonts-ttf-2.1.5.tar.gz" -P "$TEMP_DIR" && \
tar xzf "$TEMP_DIR/liberation-fonts-ttf-2.1.5.tar.gz" -C "$TEMP_DIR" && \
cp "$TEMP_DIR/liberation-fonts-ttf-2.1.5"/*.ttf "$FONT_DIR/"
;;
esac
echo "Completed installation attempt for $font_name"
}
# Enhanced language-specific font mappings
declare -A language_fonts=( declare -A language_fonts=(
["ar_AR"]="font-noto-arabic" ["ar_AR"]="font-noto-arabic"
["zh_CN"]="font-isas-misc" ["zh_CN"]="font-noto-cjk"
["zh_TW"]="font-isas-misc" ["zh_TW"]="font-noto-cjk"
["ja_JP"]="font-noto font-noto-thai font-noto-tibetan font-ipa font-sony-misc font-jis-misc" ["ja_JP"]="font-noto font-noto-cjk"
["ru_RU"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic" ["ru_RU"]="font-noto font-liberation font-vollkorn"
["sr_LATN_RS"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic" ["sr_LATN_RS"]="font-noto font-liberation"
["uk_UA"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic" ["uk_UA"]="font-noto font-liberation"
["ko_KR"]="font-noto font-noto-thai font-noto-tibetan" ["ko_KR"]="font-noto font-noto-cjk"
["el_GR"]="font-noto" ["el_GR"]="font-noto"
["hi_IN"]="font-noto-devanagari" ["hi_IN"]="font-noto-devanagari"
["bg_BG"]="font-vollkorn font-misc-cyrillic" ["bg_BG"]="font-noto font-liberation"
["GENERAL"]="font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra" ["th_TH"]="font-noto-thai"
["he_IL"]="font-noto-hebrew"
["GENERAL"]="font-noto font-dejavu font-liberation font-source-code-pro font-awesome"
) )
# Install fonts for other languages which generally do not need special packages beyond 'font-noto' # Install fonts based on specified languages
other_langs=("en_GB" "en_US" "de_DE" "fr_FR" "es_ES" "ca_CA" "it_IT" "pt_BR" "nl_NL" "sv_SE" "pl_PL" "ro_RO" "hu_HU" "tr_TR" "id_ID" "eu_ES")
if [[ $LANGS == "ALL" ]]; then if [[ $LANGS == "ALL" ]]; then
# Install all fonts from the language_fonts map # Install all fonts from the language_fonts map
declare -A installed_fonts
for fonts in "${language_fonts[@]}"; do for fonts in "${language_fonts[@]}"; do
for font in $fonts; do for font in $fonts; do
install_font $font if [[ -z "${installed_fonts[$font]}" ]]; then
install_font "$font"
installed_fonts[$font]=1
fi
done done
done done
else else
# Split comma-separated languages and install necessary fonts # Split comma-separated languages and install necessary fonts
declare -A installed_fonts
IFS=',' read -ra LANG_CODES <<< "$LANGS" IFS=',' read -ra LANG_CODES <<< "$LANGS"
for code in "${LANG_CODES[@]}"; do for code in "${LANG_CODES[@]}"; do
if [[ " ${other_langs[@]} " =~ " ${code} " ]]; then
install_font font-noto
else
fonts_to_install=${language_fonts[$code]} fonts_to_install=${language_fonts[$code]}
if [ ! -z "$fonts_to_install" ]; then if [ ! -z "$fonts_to_install" ]; then
for font in $fonts_to_install; do for font in $fonts_to_install; do
install_font $font if [[ -z "${installed_fonts[$font]}" ]]; then
done install_font "$font"
installed_fonts[$font]=1
fi fi
done
fi fi
done done
fi fi
# Cleanup
rm -rf "$TEMP_DIR"
# Update font cache
if command -v fc-cache >/dev/null; then
fc-cache -f "$FONT_DIR"
echo "Font cache updated"
else
echo "Warning: fc-cache not found. You may need to manually update your font cache"
fi
echo "Font installation completed. Fonts installed in: $FONT_DIR"

View File

@ -28,7 +28,7 @@ public class LicenseKeyChecker {
this.checkLicense(); this.checkLicense();
} }
@Scheduled(initialDelay = 604800000,fixedRate = 604800000) // 7 days in milliseconds @Scheduled(initialDelay = 604800000, fixedRate = 604800000) // 7 days in milliseconds
public void checkLicensePeriodically() { public void checkLicensePeriodically() {
checkLicense(); checkLicense();
} }

View File

@ -595,7 +595,9 @@ public class GetInfoOnPDF {
permissionsNode.put("Document Assembly", getPermissionState(ap.canAssembleDocument())); permissionsNode.put("Document Assembly", getPermissionState(ap.canAssembleDocument()));
permissionsNode.put("Extracting Content", getPermissionState(ap.canExtractContent())); permissionsNode.put("Extracting Content", getPermissionState(ap.canExtractContent()));
permissionsNode.put("Extracting for accessibility", getPermissionState(ap.canExtractForAccessibility())); permissionsNode.put(
"Extracting for accessibility",
getPermissionState(ap.canExtractForAccessibility()));
permissionsNode.put("Form Filling", getPermissionState(ap.canFillInForm())); permissionsNode.put("Form Filling", getPermissionState(ap.canFillInForm()));
permissionsNode.put("Modifying", getPermissionState(ap.canModify())); permissionsNode.put("Modifying", getPermissionState(ap.canModify()));
permissionsNode.put("Modifying annotations", getPermissionState(ap.canModifyAnnotations())); permissionsNode.put("Modifying annotations", getPermissionState(ap.canModifyAnnotations()));

View File

@ -3,11 +3,12 @@ package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey; import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
@ -41,7 +42,6 @@ import stirling.software.SPDF.service.CustomPDDocumentFactory;
@RequestMapping("/api/v1/security") @RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class ValidateSignatureController { public class ValidateSignatureController {
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
private final CertificateValidationService certValidationService; private final CertificateValidationService certValidationService;
@ -56,7 +56,7 @@ public class ValidateSignatureController {
@Operation( @Operation(
summary = "Validate PDF Digital Signature", summary = "Validate PDF Digital Signature",
description = description =
"Validates the digital signatures in a PDF file against default or custom certificates. Input:PDF Output:JSON Type:SISO") "Validates digital signatures in a PDF file against default or custom certificates.")
@PostMapping(value = "/validate-signature") @PostMapping(value = "/validate-signature")
public ResponseEntity<List<SignatureValidationResult>> validateSignature( public ResponseEntity<List<SignatureValidationResult>> validateSignature(
@ModelAttribute SignatureValidationRequest request) throws IOException { @ModelAttribute SignatureValidationRequest request) throws IOException {
@ -80,7 +80,6 @@ public class ValidateSignatureController {
for (PDSignature sig : signatures) { for (PDSignature sig : signatures) {
SignatureValidationResult result = new SignatureValidationResult(); SignatureValidationResult result = new SignatureValidationResult();
try { try {
byte[] signedContent = sig.getSignedContent(file.getInputStream()); byte[] signedContent = sig.getSignedContent(file.getInputStream());
byte[] signatureBytes = sig.getContents(file.getInputStream()); byte[] signatureBytes = sig.getContents(file.getInputStream());
@ -92,49 +91,75 @@ public class ValidateSignatureController {
SignerInformationStore signerStore = signedData.getSignerInfos(); SignerInformationStore signerStore = signedData.getSignerInfos();
for (SignerInformation signer : signerStore.getSigners()) { for (SignerInformation signer : signerStore.getSigners()) {
X509CertificateHolder certHolder = (X509CertificateHolder) certStore.getMatches(signer.getSID()).iterator().next(); X509CertificateHolder certHolder =
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certHolder); (X509CertificateHolder)
certStore.getMatches(signer.getSID()).iterator().next();
X509Certificate cert =
new JcaX509CertificateConverter().getCertificate(certHolder);
boolean isValid = signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert)); // Basic signature validation
result.setValid(isValid); result.setValid(
signer.verify(
new JcaSimpleSignerInfoVerifierBuilder().build(cert)));
// Additional validations // Perform chain validation
result.setChainValid(customCert != null CertificateValidationService.ValidationResult chainResult;
? certValidationService.validateCertificateChainWithCustomCert(cert, customCert) if (customCert != null) {
: certValidationService.validateCertificateChain(cert)); chainResult =
certValidationService.validateWithCustomCert(cert, customCert);
} else {
chainResult = certValidationService.validateCertificateChain(cert);
}
result.setTrustValid(customCert != null result.setChainValid(chainResult.isValid());
? certValidationService.validateTrustWithCustomCert(cert, customCert) result.setTrustValid(chainResult.isValid());
: certValidationService.validateTrustStore(cert)); result.setNotExpired(!chainResult.isExpired());
result.setNotRevoked(!certValidationService.isRevoked(cert)); // Check if signature was valid at the time of signing
result.setNotExpired(!cert.getNotAfter().before(new Date())); if (sig.getSignDate() != null) {
try {
cert.checkValidity(sig.getSignDate().getTime());
result.setValidAtTimeOfSigning(true);
} catch (CertificateExpiredException
| CertificateNotYetValidException e) {
result.setValidAtTimeOfSigning(false);
}
}
// Set basic signature info // Set signature info
populateSignatureInfo(result, sig, cert);
}
} catch (Exception e) {
result.setValid(false);
result.setErrorMessage("Signature validation failed: " + e.getMessage());
}
results.add(result);
}
}
return ResponseEntity.ok(results);
}
private void populateSignatureInfo(
SignatureValidationResult result, PDSignature sig, X509Certificate cert) {
result.setSignerName(sig.getName()); result.setSignerName(sig.getName());
result.setSignatureDate(sig.getSignDate().getTime().toString()); result.setSignatureDate(sig.getSignDate().getTime().toString());
result.setReason(sig.getReason()); result.setReason(sig.getReason());
result.setLocation(sig.getLocation()); result.setLocation(sig.getLocation());
// Set new certificate details
result.setIssuerDN(cert.getIssuerX500Principal().getName()); result.setIssuerDN(cert.getIssuerX500Principal().getName());
result.setSubjectDN(cert.getSubjectX500Principal().getName()); result.setSubjectDN(cert.getSubjectX500Principal().getName());
result.setSerialNumber(cert.getSerialNumber().toString(16)); // Hex format result.setSerialNumber(cert.getSerialNumber().toString(16));
result.setValidFrom(cert.getNotBefore().toString()); result.setValidFrom(cert.getNotBefore().toString());
result.setValidUntil(cert.getNotAfter().toString()); result.setValidUntil(cert.getNotAfter().toString());
result.setSignatureAlgorithm(cert.getSigAlgName()); result.setSignatureAlgorithm(cert.getSigAlgName());
// Get key size (if possible)
try { try {
result.setKeySize(((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength()); result.setKeySize(((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength());
} catch (Exception e) { } catch (Exception e) {
// If not RSA or error, set to 0
result.setKeySize(0); result.setKeySize(0);
} }
result.setVersion(String.valueOf(cert.getVersion())); result.setVersion(String.valueOf(cert.getVersion()));
// Set key usage
List<String> keyUsages = new ArrayList<>(); List<String> keyUsages = new ArrayList<>();
boolean[] keyUsageFlags = cert.getKeyUsage(); boolean[] keyUsageFlags = cert.getKeyUsage();
if (keyUsageFlags != null) { if (keyUsageFlags != null) {
@ -150,19 +175,6 @@ public class ValidateSignatureController {
} }
} }
result.setKeyUsages(keyUsages); result.setKeyUsages(keyUsages);
// Check if self-signed
result.setSelfSigned(cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal())); result.setSelfSigned(cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal()));
} }
} catch (Exception e) {
result.setValid(false);
result.setErrorMessage("Signature validation failed: " + e.getMessage());
}
results.add(result);
}
}
return ResponseEntity.ok(results);
}
} }

View File

@ -16,16 +16,16 @@ public class SignatureValidationResult {
private boolean trustValid; private boolean trustValid;
private boolean notExpired; private boolean notExpired;
private boolean notRevoked; private boolean notRevoked;
private boolean validAtTimeOfSigning;
private String issuerDN; // Certificate issuer's Distinguished Name private String issuerDN; // Certificate issuer's Distinguished Name
private String subjectDN; // Certificate subject's Distinguished Name private String subjectDN; // Certificate subject's Distinguished Name
private String serialNumber; // Certificate serial number private String serialNumber; // Certificate serial number
private String validFrom; // Certificate validity start date private String validFrom; // Certificate validity start date
private String validUntil; // Certificate validity end date private String validUntil; // Certificate validity end date
private String signatureAlgorithm;// Algorithm used for signing private String signatureAlgorithm; // Algorithm used for signing
private int keySize; // Key size in bits private int keySize; // Key size in bits
private String version; // Certificate version private String version; // Certificate version
private List<String> keyUsages; // List of key usage purposes private List<String> keyUsages; // List of key usage purposes
private boolean isSelfSigned; // Whether the certificate is self-signed private boolean isSelfSigned; // Whether the certificate is self-signed
} }

View File

@ -1,13 +1,11 @@
package stirling.software.SPDF.service; package stirling.software.SPDF.service;
import io.github.pixee.security.BoundedLineReader; import java.io.*;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.security.cert.*;
import java.security.cert.CertPath; import java.security.cert.CertPath;
import java.security.cert.CertPathValidator; import java.security.cert.CertPathValidator;
import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateExpiredException;
@ -16,84 +14,126 @@ import java.security.cert.CertificateNotYetValidException;
import java.security.cert.PKIXParameters; import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor; import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.Arrays; import java.util.*;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@Service @Service
@Slf4j
public class CertificateValidationService { public class CertificateValidationService {
private KeyStore trustStore; private KeyStore trustStore;
private static final String AATL_RESOURCE = "/tl12.acrobatsecuritysettings";
@PostConstruct @PostConstruct
private void initializeTrustStore() throws Exception { private void initializeTrustStore() throws Exception {
trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null); trustStore.load(null, null);
loadMozillaCertificates(); loadAATLCertificatesFromPDF();
} }
private void loadMozillaCertificates() throws Exception { private void loadAATLCertificatesFromPDF() throws Exception {
try (InputStream is = getClass().getResourceAsStream("/certdata.txt")) { log.debug("Starting AATL certificate loading from PDF...");
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line; try (InputStream pdfStream = new ClassPathResource(AATL_RESOURCE).getInputStream()) {
StringBuilder certData = new StringBuilder(); PDDocument document = Loader.loadPDF(pdfStream.readAllBytes());
boolean inCert = false;
PDEmbeddedFilesNameTreeNode embeddedFiles =
document.getDocumentCatalog().getNames().getEmbeddedFiles();
Map<String, PDComplexFileSpecification> files = embeddedFiles.getNames();
for (Map.Entry<String, PDComplexFileSpecification> entry : files.entrySet()) {
log.debug(entry.getKey());
if (entry.getKey().equals("SecuritySettings.xml")) {
byte[] xmlContent = entry.getValue().getEmbeddedFile().toByteArray();
processSecuritySettingsXML(xmlContent);
break;
}
}
}
}
private void processSecuritySettingsXML(byte[] xmlContent) throws Exception {
// Simple XML parsing using String operations
String xmlString = new String(xmlContent, "UTF-8");
int certCount = 0; int certCount = 0;
int failedCerts = 0;
while ((line = BoundedLineReader.readLine(reader, 5_000_000)) != null) { // Find all Certificate tags
if (line.startsWith("CKA_VALUE MULTILINE_OCTAL")) { String startTag = "<Certificate>";
inCert = true; String endTag = "</Certificate>";
certData = new StringBuilder(); int startIndex = 0;
continue;
} while ((startIndex = xmlString.indexOf(startTag, startIndex)) != -1) {
if (inCert) { int endIndex = xmlString.indexOf(endTag, startIndex);
if ("END".equals(line)) { if (endIndex == -1) break;
inCert = false;
byte[] certBytes = parseOctalData(certData.toString()); // Extract certificate data
if (certBytes != null) { String certData = xmlString.substring(startIndex + startTag.length(), endIndex).trim();
startIndex = endIndex + endTag.length();
try {
byte[] certBytes = Base64.getDecoder().decode(certData);
CertificateFactory cf = CertificateFactory.getInstance("X.509"); CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert = X509Certificate cert =
(X509Certificate) (X509Certificate)
cf.generateCertificate( cf.generateCertificate(new ByteArrayInputStream(certBytes));
new ByteArrayInputStream(certBytes));
trustStore.setCertificateEntry("mozilla-cert-" + certCount++, cert);
}
} else {
certData.append(line).append("\n");
}
}
}
}
}
private byte[] parseOctalData(String data) { // Only store root certificates (self-signed)
try { if (cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal())) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); trustStore.setCertificateEntry("aatl-cert-" + certCount, cert);
String[] tokens = data.split("\\\\"); log.trace(
for (String token : tokens) { "Successfully loaded AATL root certificate #"
token = token.trim(); + certCount
if (!token.isEmpty()) { + "\n Subject: "
baos.write(Integer.parseInt(token, 8)); + cert.getSubjectX500Principal().getName()
+ "\n Valid until: "
+ cert.getNotAfter());
certCount++;
} }
}
return baos.toByteArray();
} catch (Exception e) { } catch (Exception e) {
return null; failedCerts++;
log.error("Failed to process AATL certificate: " + e.getMessage());
} }
} }
public boolean validateCertificateChain(X509Certificate cert) { log.debug("AATL Certificate loading completed:");
log.debug(" Total root certificates successfully loaded: " + certCount);
log.debug(" Failed certificates: " + failedCerts);
}
@Data
public static class ValidationResult {
private boolean valid;
private boolean expired;
private boolean validAtSigningTime;
private String errorMessage;
}
public ValidationResult validateCertificateChain(X509Certificate signerCert) {
ValidationResult result = new ValidationResult();
try { try {
CertPathValidator validator = CertPathValidator.getInstance("PKIX"); // Build the certificate chain
CertificateFactory cf = CertificateFactory.getInstance("X.509"); List<X509Certificate> certChain = buildCertificateChain(signerCert);
List<X509Certificate> certList = Arrays.asList(cert);
CertPath certPath = cf.generateCertPath(certList);
// Create certificate path
CertificateFactory cf = CertificateFactory.getInstance("X.509");
CertPath certPath = cf.generateCertPath(certChain);
// Set up trust anchors
Set<TrustAnchor> anchors = new HashSet<>(); Set<TrustAnchor> anchors = new HashSet<>();
Enumeration<String> aliases = trustStore.aliases(); Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) { while (aliases.hasMoreElements()) {
@ -103,31 +143,113 @@ public class CertificateValidationService {
} }
} }
// Set up validation parameters
PKIXParameters params = new PKIXParameters(anchors); PKIXParameters params = new PKIXParameters(anchors);
params.setRevocationEnabled(false); params.setRevocationEnabled(false);
// Validate the path
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
validator.validate(certPath, params); validator.validate(certPath, params);
return true;
result.setValid(true);
result.setExpired(isExpired(signerCert));
return result;
} catch (Exception e) { } catch (Exception e) {
return false; result.setValid(false);
result.setErrorMessage(e.getMessage());
return result;
} }
} }
public boolean validateTrustStore(X509Certificate cert) { public ValidationResult validateWithCustomCert(
X509Certificate signerCert, X509Certificate customCert) {
ValidationResult result = new ValidationResult();
try {
// Build the complete chain from signer cert
List<X509Certificate> certChain = buildCertificateChain(signerCert);
// Check if custom cert matches any cert in the chain
boolean matchFound = false;
for (X509Certificate chainCert : certChain) {
if (chainCert.equals(customCert)) {
matchFound = true;
break;
}
}
if (!matchFound) {
// Check if custom cert is a valid issuer for any cert in the chain
for (X509Certificate chainCert : certChain) {
try {
chainCert.verify(customCert.getPublicKey());
matchFound = true;
break;
} catch (Exception e) {
// Continue checking next cert
}
}
}
result.setValid(matchFound);
if (!matchFound) {
result.setErrorMessage(
"Custom certificate is not part of the chain and is not a valid issuer");
}
return result;
} catch (Exception e) {
result.setValid(false);
result.setErrorMessage(e.getMessage());
return result;
}
}
private List<X509Certificate> buildCertificateChain(X509Certificate signerCert)
throws CertificateException {
List<X509Certificate> chain = new ArrayList<>();
chain.add(signerCert);
X509Certificate current = signerCert;
while (!isSelfSigned(current)) {
X509Certificate issuer = findIssuer(current);
if (issuer == null) break;
chain.add(issuer);
current = issuer;
}
return chain;
}
private boolean isSelfSigned(X509Certificate cert) {
return cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal());
}
private X509Certificate findIssuer(X509Certificate cert) throws CertificateException {
try { try {
Enumeration<String> aliases = trustStore.aliases(); Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) { while (aliases.hasMoreElements()) {
Object trustCert = trustStore.getCertificate(aliases.nextElement()); Certificate trustCert = trustStore.getCertificate(aliases.nextElement());
if (trustCert instanceof X509Certificate && cert.equals(trustCert)) { if (trustCert instanceof X509Certificate) {
return true; X509Certificate x509TrustCert = (X509Certificate) trustCert;
if (cert.getIssuerX500Principal()
.equals(x509TrustCert.getSubjectX500Principal())) {
try {
cert.verify(x509TrustCert.getPublicKey());
return x509TrustCert;
} catch (Exception e) {
// Continue searching if verification fails
}
}
} }
} }
return false;
} catch (KeyStoreException e) { } catch (KeyStoreException e) {
return false; throw new CertificateException("Error accessing trust store", e);
} }
return null;
} }
public boolean isRevoked(X509Certificate cert) { private boolean isExpired(X509Certificate cert) {
try { try {
cert.checkValidity(); cert.checkValidity();
return false; return false;
@ -135,23 +257,4 @@ public class CertificateValidationService {
return true; return true;
} }
} }
public boolean validateCertificateChainWithCustomCert(
X509Certificate cert, X509Certificate customCert) {
try {
cert.verify(customCert.getPublicKey());
return true;
} catch (Exception e) {
return false;
}
}
public boolean validateTrustWithCustomCert(X509Certificate cert, X509Certificate customCert) {
try {
// Compare the issuer of the signature certificate with the custom certificate
return cert.getIssuerX500Principal().equals(customCert.getSubjectX500Principal());
} catch (Exception e) {
return false;
}
}
} }

Binary file not shown.