---
- become: false
  delegate_to: localhost
  vars:
    ansible_python_interpreter: "{{ ansible_playbook_python }}"
    certificate_validity_days: 3650  # 10 years - configurable certificate lifespan
  block:
    - debug: var=subjectAltName

    - name: Ensure the pki directory does not exist
      file:
        dest: "{{ ipsec_pki_path }}"
        state: absent
      when: keys_clean_all|bool

    - name: Ensure the pki directories exist
      file:
        dest: "{{ ipsec_pki_path }}/{{ item }}"
        state: directory
        recurse: true
        mode: "0700"
      loop:
        - certs
        - private
        - public

    - name: Ensure the config directories exist
      file:
        dest: "{{ ipsec_config_path }}/{{ item }}"
        state: directory
        recurse: true
        mode: "0700"
      loop:
        - apple
        - manual

    - name: Create private key with password protection
      community.crypto.openssl_privatekey:
        path: "{{ ipsec_pki_path }}/private/cakey.pem"
        passphrase: "{{ CA_password }}"
        type: ECC
        curve: secp384r1
        mode: "0600"
      no_log: true

    # CA certificate with name constraints to prevent certificate misuse (Issue #75)
    - name: Create certificate signing request (CSR) for CA certificate with security constraints
      community.crypto.openssl_csr_pipe:
        privatekey_path: "{{ ipsec_pki_path }}/private/cakey.pem"
        privatekey_passphrase: "{{ CA_password }}"
        common_name: "{{ IP_subject_alt_name }}"
        use_common_name_for_san: true
        # Generate Subject Key Identifier for proper Authority Key Identifier creation
        create_subject_key_identifier: true
        basic_constraints:
          - 'CA:TRUE'
          - 'pathlen:0'        # Prevents sub-CA creation - limits certificate chain depth if CA key compromised
        basic_constraints_critical: true
        key_usage:
          - keyCertSign
          - cRLSign
        key_usage_critical: true
        # CA restricted to VPN certificate issuance only
        extended_key_usage:
          - '1.3.6.1.5.5.7.3.17'        # IPsec End Entity OID - VPN-specific usage
        extended_key_usage_critical: true
        # Name Constraints: Defense-in-depth security restricting certificate scope to prevent misuse
        # Limits CA to only issue certificates for this specific VPN deployment's resources
        # Per-deployment UUID prevents cross-deployment reuse, unique email domain isolates certificate scope
        name_constraints_permitted: >-
          {{ [
            subjectAltName_type + ':' + IP_subject_alt_name + ('/255.255.255.255' if subjectAltName_type == 'IP' else ''),
            'DNS:' + openssl_constraint_random_id,
            'email:' + openssl_constraint_random_id
          ] + (
            ['IP:' + ansible_default_ipv6['address'] + '/128'] if ipv6_support | bool else []
          ) }}
        # Block public domains/networks to prevent certificate abuse for impersonation attacks
        # Public TLD exclusion, Email domain exclusion, RFC 1918: prevents lateral movement
        # IPv6: ULA/link-local/doc ranges or all
        name_constraints_excluded: >-
          {{ [
            'DNS:.com', 'DNS:.org', 'DNS:.net', 'DNS:.gov', 'DNS:.edu', 'DNS:.mil', 'DNS:.int',
            'email:.com', 'email:.org', 'email:.net', 'email:.gov', 'email:.edu', 'email:.mil', 'email:.int',
            'IP:10.0.0.0/255.0.0.0', 'IP:172.16.0.0/255.240.0.0', 'IP:192.168.0.0/255.255.0.0'
          ] + (
            ['IP:fc00::/7', 'IP:fe80::/10', 'IP:2001:db8::/32'] if ipv6_support | bool else ['IP:::/0']
          ) }}
        name_constraints_critical: true
      register: ca_csr

    - name: Create self-signed CA certificate from CSR
      community.crypto.x509_certificate:
        path: "{{ ipsec_pki_path }}/cacert.pem"
        csr_content: "{{ ca_csr.csr }}"
        privatekey_path: "{{ ipsec_pki_path }}/private/cakey.pem"
        privatekey_passphrase: "{{ CA_password }}"
        provider: selfsigned
        mode: "0644"
      no_log: true

    - name: Copy the CA certificate
      copy:
        src: "{{ ipsec_pki_path }}/cacert.pem"
        dest: "{{ ipsec_config_path }}/manual/cacert.pem"
        mode: '0644'

    - name: Create private keys for users and server
      community.crypto.openssl_privatekey:
        path: "{{ ipsec_pki_path }}/private/{{ item }}.key"
        type: ECC
        curve: secp384r1
        mode: "0600"
      loop: "{{ users + [IP_subject_alt_name] }}"
      register: client_key_jobs

    # Server certificate with SAN extension - required for modern Apple devices
    - name: Create CSRs for server certificate with SAN
      community.crypto.openssl_csr_pipe:
        privatekey_path: "{{ ipsec_pki_path }}/private/{{ IP_subject_alt_name }}.key"
        subject_alt_name: "{{ subjectAltName.split(',') }}"
        common_name: "{{ IP_subject_alt_name }}"
        # Add Basic Constraints to prevent certificate chain validation errors
        basic_constraints:
          - 'CA:FALSE'
        basic_constraints_critical: false
        key_usage:
          - digitalSignature
          - keyEncipherment
        key_usage_critical: false
        # Server auth EKU required for IKEv2 server certificates (Issue #75)
        # NOTE: clientAuth deliberately excluded to prevent role confusion attacks
        extended_key_usage:
          - serverAuth                    # Server Authentication (RFC 5280)
          - '1.3.6.1.5.5.7.3.17'        # IPsec End Entity (RFC 4945)
        extended_key_usage_critical: false
      register: server_csr

    - name: Create CSRs for client certificates
      community.crypto.openssl_csr_pipe:
        privatekey_path: "{{ ipsec_pki_path }}/private/{{ item }}.key"
        subject_alt_name:
          - "email:{{ item }}@{{ openssl_constraint_random_id }}"  # UUID domain prevents certificate reuse across deployments
        common_name: "{{ item }}"
        # Add Basic Constraints to client certificates for proper PKI validation
        basic_constraints:
          - 'CA:FALSE'
        basic_constraints_critical: false
        key_usage:
          - digitalSignature
          - keyEncipherment
        key_usage_critical: false
        # Client certs restricted to clientAuth only - prevents clients from impersonating the VPN server
        # NOTE: serverAuth deliberately excluded to prevent server impersonation attacks
        extended_key_usage:
          - clientAuth                    # Client Authentication (RFC 5280)
          - '1.3.6.1.5.5.7.3.17'        # IPsec End Entity (RFC 4945)
        extended_key_usage_critical: false
      loop: "{{ users }}"
      register: client_csr_jobs

    - name: Sign server certificate with CA
      community.crypto.x509_certificate:
        csr_content: "{{ server_csr.csr }}"
        path: "{{ ipsec_pki_path }}/certs/{{ IP_subject_alt_name }}.crt"
        provider: ownca
        ownca_path: "{{ ipsec_pki_path }}/cacert.pem"
        ownca_privatekey_path: "{{ ipsec_pki_path }}/private/cakey.pem"
        ownca_privatekey_passphrase: "{{ CA_password }}"
        ownca_not_after: "+{{ certificate_validity_days }}d"
        ownca_not_before: "-1d"
        mode: "0644"
      no_log: true

    - name: Sign client certificates with CA
      community.crypto.x509_certificate:
        csr_content: "{{ item.csr }}"
        path: "{{ ipsec_pki_path }}/certs/{{ item.item }}.crt"
        provider: ownca
        ownca_path: "{{ ipsec_pki_path }}/cacert.pem"
        ownca_privatekey_path: "{{ ipsec_pki_path }}/private/cakey.pem"
        ownca_privatekey_passphrase: "{{ CA_password }}"
        ownca_not_after: "+{{ certificate_validity_days }}d"
        ownca_not_before: "-1d"
        mode: "0644"
      loop: "{{ client_csr_jobs.results }}"
      register: client_sign_results
      no_log: true

    - name: Generate p12 files
      community.crypto.openssl_pkcs12:
        path: "{{ ipsec_pki_path }}/private/{{ item }}.p12"
        friendly_name: "{{ item }}"
        privatekey_path: "{{ ipsec_pki_path }}/private/{{ item }}.key"
        certificate_path: "{{ ipsec_pki_path }}/certs/{{ item }}.crt"
        passphrase: "{{ p12_export_password }}"
        mode: "0600"
        encryption_level: "compatibility2022"  # Apple device compatibility
      loop: "{{ users }}"
      no_log: true

    - name: Generate p12 files with CA certificate included
      community.crypto.openssl_pkcs12:
        path: "{{ ipsec_pki_path }}/private/{{ item }}_ca.p12"
        friendly_name: "{{ item }}"
        privatekey_path: "{{ ipsec_pki_path }}/private/{{ item }}.key"
        certificate_path: "{{ ipsec_pki_path }}/certs/{{ item }}.crt"
        other_certificates:
          - "{{ ipsec_pki_path }}/cacert.pem"
        passphrase: "{{ p12_export_password }}"
        mode: "0600"
        encryption_level: "compatibility2022"  # Apple device compatibility
      loop: "{{ users }}"
      no_log: true

    - name: Copy the p12 certificates
      copy:
        src: "{{ ipsec_pki_path }}/private/{{ item }}.p12"
        dest: "{{ ipsec_config_path }}/manual/{{ item }}.p12"
        mode: '0600'
      loop: "{{ users }}"

    - name: Build openssh public keys
      community.crypto.openssl_publickey:
        path: "{{ ipsec_pki_path }}/public/{{ item }}.pub"
        privatekey_path: "{{ ipsec_pki_path }}/private/{{ item }}.key"
        format: OpenSSH
      loop: "{{ users }}"

    - name: Add all users to the file
      ansible.builtin.lineinfile:
        path: "{{ ipsec_pki_path }}/all-users"
        line: "{{ item }}"
        mode: '0644'
        create: true
      loop: "{{ users }}"
      register: users_file

    - name: Set all users as a fact
      set_fact:
        all_users: "{{ lookup('file', ipsec_pki_path + '/all-users').splitlines() }}"

    # Certificate Revocation List (CRL) for removed users
    - name: Calculate current timestamp for CRL
      set_fact:
        crl_timestamp: "{{ '%Y%m%d%H%M%SZ' | strftime(ansible_date_time.epoch | int) }}"

    - name: Identify users whose certificates need revocation
      set_fact:
        users_to_revoke: "{{ all_users | difference(users) }}"

    - name: Build revoked certificates list
      set_fact:
        revoked_certificates: >-
          {{ users_to_revoke | map('regex_replace', '^(.*)$',
             '{"path": "' + ipsec_pki_path + '/certs/\1.crt", "revocation_date": "' + crl_timestamp + '"}') | list }}

    - name: Generate a CRL
      community.crypto.x509_crl:
        path: "{{ ipsec_pki_path }}/crl.pem"
        privatekey_path: "{{ ipsec_pki_path }}/private/cakey.pem"
        privatekey_passphrase: "{{ CA_password }}"
        last_update: "{{ '%Y%m%d%H%M%SZ' | strftime(ansible_date_time.epoch | int) }}"
        next_update: "{{ '%Y%m%d%H%M%SZ' | strftime((ansible_date_time.epoch | int) + (10 * 365 * 24 * 60 * 60)) }}"
        crl_mode: generate
        issuer:
          CN: "{{ IP_subject_alt_name }}"
        revoked_certificates: "{{ revoked_certificates }}"
      no_log: true

    - name: Set CRL file permissions
      file:
        path: "{{ ipsec_pki_path }}/crl.pem"
        mode: "0644"

- name: Copy the CRL to the vpn server
  copy:
    src: "{{ ipsec_pki_path }}/crl.pem"
    dest: "{{ config_prefix | default('/') }}etc/ipsec.d/crls/algo.root.pem"
    mode: '0644'
  notify:
    - rereadcrls
