diff --git a/config.template.yml b/config.template.yml index 85f1bea72..9fb6c8e72 100644 --- a/config.template.yml +++ b/config.template.yml @@ -815,36 +815,79 @@ notifier: ## HMAC Secret can also be set using a secret: https://www.authelia.com/c/secrets # hmac_secret: this_is_a_secret_abc123abc123abc + ## The issuer_certificate_chain is an optional PEM encoded certificate chain. It's used in conjunction with the + ## issuer_private_key to sign JWT's. All certificates in the chain must be within the validity period, and every + ## certificate included must be signed by the certificate immediately after it if provided. + # issuer_certificate_chain: | + # -----BEGIN CERTIFICATE----- + # MIIC5jCCAc6gAwIBAgIRAK4Sj7FiN6PXo/urPfO4E7owDQYJKoZIhvcNAQELBQAw + # EzERMA8GA1UEChMIQXV0aGVsaWEwHhcNNzAwMTAxMDAwMDAwWhcNNzEwMTAxMDAw + # MDAwWjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQADggEP + # ADCCAQoCggEBAPKv3pSyP4ozGEiVLJ14dIWFCEGEgq7WUMI0SZZqQA2ID0L59U/Q + # /Usyy7uC9gfMUzODTpANtkOjFQcQAsxlR1FOjVBrX5QgjSvXwbQn3DtwMA7XWSl6 + # LuYx2rBYSlMSN5UZQm/RxMtXfLK2b51WgEEYDFi+nECSqKzR4R54eOPkBEWRfvuY + # 91AMjlhpivg8e4JWkq4LVQUKbmiFYwIdK8XQiN4blY9WwXwJFYs5sQ/UYMwBFi0H + # kWOh7GEjfxgoUOPauIueZSMSlQp7zqAH39N0ZSYb6cS0Npj57QoWZSY3ak87ebcR + # Nf4rCvZLby7LoN7qYCKxmCaDD3x2+NYpWH8CAwEAAaM1MDMwDgYDVR0PAQH/BAQD + # AgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcN + # AQELBQADggEBAHSITqIQSNzonFl3DzxHPEzr2hp6peo45buAAtu8FZHoA+U7Icfh + # /ZXjPg7Xz+hgFwM/DTNGXkMWacQA/PaNWvZspgRJf2AXvNbMSs2UQODr7Tbv+Fb4 + # lyblmMUNYFMCFVAMU0eIxXAFq2qcwv8UMcQFT0Z/35s6PVOakYnAGGQjTfp5Ljuq + # wsdc/xWmM0cHWube6sdRRUD7SY20KU/kWzl8iFO0VbSSrDf1AlEhnLEkp1SPaxXg + # OdBnl98MeoramNiJ7NT6Jnyb3zZ578fjaWfThiBpagItI8GZmG4s4Ovh2JbheN8i + # ZsjNr9jqHTjhyLVbDRlmJzcqoj4JhbKs6/I^invalid DO NOT USE= + # -----END CERTIFICATE----- + # -----BEGIN CERTIFICATE----- + # MIIDBDCCAeygAwIBAgIRALJsPg21kA0zY4F1wUCIuoMwDQYJKoZIhvcNAQELBQAw + # EzERMA8GA1UEChMIQXV0aGVsaWEwHhcNNzAwMTAxMDAwMDAwWhcNNzEwMTAxMDAw + # MDAwWjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQADggEP + # ADCCAQoCggEBAMXHBvVxUzYk0u34/DINMSF+uiOekKOAjOrC6Mi9Ww8ytPVO7t2S + # zfTvM+XnEJqkFQFgimERfG/eGhjF9XIEY6LtnXe8ATvOK4nTwdufzBaoeQu3Gd50 + # 5VXr6OHRo//ErrGvFXwP3g8xLePABsi/fkH3oDN+ztewOBMDzpd+KgTrk8ysv2ou + # kNRMKFZZqASvCgv0LD5KWvUCnL6wgf1oTXG7aztduA4oSkUP321GpOmBC5+5ElU7 + # ysoRzvD12o9QJ/IfEaulIX06w9yVMo60C/h6A3U6GdkT1SiyTIqR7v7KU/IWd/Qi + # Lfftcj91VhCmJ73Meff2e2S2PrpjdXbG5FMCAwEAAaNTMFEwDgYDVR0PAQH/BAQD + # AgKkMA8GA1UdJQQIMAYGBFUdJQAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU + # Z7AtA3mzFc0InSBA5fiMfeLXA3owDQYJKoZIhvcNAQELBQADggEBAEE5hm1mtlk/ + # kviCoHH4evbpw7rxPxDftIQlqYTtvMM4eWY/6icFoSZ4fUHEWYyps8SsPu/8f2tf + # 71LGgZn0FdHi1QU2H8m0HHK7TFw+5Q6RLrLdSyk0PItJ71s9en7r8pX820nAFEHZ + # HkOSfJZ7B5hFgUDkMtVM6bardXAhoqcMk4YCU96e9d4PB4eI+xGc+mNuYvov3RbB + # D0s8ICyojeyPVLerz4wHjZu68Z5frAzhZ68YbzNs8j2fIBKKHkHyLG1iQyF+LJVj + # 2PjCP+auJsj6fQQpMGoyGtpLcSDh+ptcTngUD8JsWipzTCjmaNqdPHAOYmcgtf4b + # qocikt3WAdU^invalid DO NOT USE= + # -----END CERTIFICATE----- + ## The issuer_private_key is used to sign the JWT forged by OpenID Connect. ## Issuer Private Key can also be set using a secret: https://www.authelia.com/c/secrets # issuer_private_key: | # -----BEGIN RSA PRIVATE KEY----- - # MXIEogIB$AKCAQEAxZVJP3WF//PG2fLQoEC9DtdiFG/+00vqlbVzz47nyxKONIPI - # lmL3UdmqpGTKMe/5Brqse4ZAKlQHiDbwzK9ypnfigtHuvh/JO0S7ChP70RC67ed1 - # HV1nyfz5eW3llbtGJPrlYLqITNgctHp6zmRUFtSzPj9qFvozI93LJi492yL1+vu8 - # Un3Dm8+Qq6XM2tPdEcldB/dtBwOWoF+8eOOVsu0TDuB5bwlhBVGJuSAuzBPRS2bF - # Ga4uk0JDdkDOMCEQxC5uWDFxgfERSMFyfLVWD47woDbuWEBq10c0z+dpWPMp7Ain - # YnnkqicwCN88Z0zid6MmMQ65F4+9Hc+qC/p6xwIDAQABAoIBAGlhaAHKor+Su3o/ - # AXqXTL5/rbYMzbLQiLt0XeJT69jpeqMTroZXHmWvXE3128mqnf0yzw/K2Ko6yxGh - # i+j/onya8FqpsVYCCgfsbn2/js1AyRJeIp6Y1ORsYnqbXJnxmkXa80AV/OBPW2/+ - # 60TtSdQrebY3iFPc+i2k+9bPTvpyyDLKlz8UwdZG+k5uyYNIyQTccz+PjwsIvDij - # 7tKYamhhLN3QXt3/aZTFpjTgezP4WyriZxjWrddHowc47q2rwNS95ND39JcysJAc - # 0Pcbu8A5lVa7Fx33uOtzDfKWIW7xVEN+OtPgN+FbTjXcXk5IZedl+pW5lU5P++G/ - # ZPvz+WECgYEA9g6HwdODW3e68bOqsFoKg35+vfUFMzlyMF8HFylNVfnLpTEDr637 - # owzMFvcUxVd71b+gV5nnnbI+riUFIgyR8vhCjhy4moopDPahC4/KwN4NG6uz+i1h - # AB6D5+zn2BjnO/5xMMFGlApWtRNmJVGYlNDj3bXKh2VXzzy03VNeD8kCgYEAzZFL - # OlzoRB1HKpTWIECcuvxofMxLOLb3zs0k2t/FYNYIpovmGWCCAULz13y53e5+/+5m - # 7I9VUZJFaIhaZ36qVBApCKdru69pZMkWCcQO9jELFcx51Ez7OgJWzu7GS1QJCPKC - # fEDxI0rZK21j93/Sl/nUnEir7CYpQ+wvCaGuHg8CgYAXgbncfY1+DokwkB6NbHy2 - # pT4Mfbz6cNGE538w6kQ2I4AeDvmwLentYMqaow478CinegAiflSPTzkHwAemghbr - # ZGZPV1UXhn13fJRUG2+eT1hnPVcbXnx223N0k8Bud6qXo65CnyRT/kzcTbcjd5Eh - # Hne2daicmMTzynPo9Q72aQKBgBmobO9X8VWvIdbaxO85oVZlctVA2pK1o7CYQmVf - # UM+JZ4MCKzI3rYJizPS0iK5+ujNPmmEkcs2/qBIoEsCgOrpLWhPOcc/3UPxXbPzD - # D+sCrBOIdhxdj23qJNOnUfDNCGOpgUfpAzAYg4q8GKInvi1h7XukRnEvQi9MJ4LY - # P1dZAoGASGcGnTMkmeSXP8ux+dvQJAiJskn/sJIgBZ5uq5GRCeLBUosRSVxM75UK - # vAh/c/RBj+pYXVKuPuHGZCQJxsdcRXzXNGouUtgbaYML5Me/Hagt20QzDRBfuGBg - # qeZBJaXhjElvw6PUWtg4x+LYRCBpq/bS3LK3ozZrSTukVkKDegw= - # -----END RSA PRIVATE KEY----- + # MIIEpAIBAAKCAQEA8q/elLI/ijMYSJUsnXh0hYUIQYSCrtZQwjRJlmpADYgPQvn1 + # T9D9SzLLu4L2B8xTM4NOkA22Q6MVBxACzGVHUU6NUGtflCCNK9fBtCfcO3AwDtdZ + # KXou5jHasFhKUxI3lRlCb9HEy1d8srZvnVaAQRgMWL6cQJKorNHhHnh44+QERZF+ + # +5j3UAyOWGmK+Dx7glaSrgtVBQpuaIVjAh0rxdCI3huVj1bBfAkVizmxD9RgzAEW + # LQeRY6HsYSN/GChQ49q4i55lIxKVCnvOoAff03RlJhvpxLQ2mPntChZlJjdqTzt5 + # txE1/isK9ktvLsug3upgIrGYJoMPfHb41ilYfwIDAQABAoIBAQDTOdFf2JjHH1um + # aPgRAvNf9v7Nj5jytaRKs5nM6iNf46ls4QPreXnMhqSeSwj6lpNgBYxOgzC9Q+cc + # Y4ob/paJJPaIJTxmP8K/gyWcOQlNToL1l+eJ20eQoZm23NGr5fIsunSBwLEpTrdB + # ENqqtcwhW937K8Pxy/Q1nuLyU2bc6Tn/ivLozc8n27dpQWWKh8537VY7ancIaACr + # LJJLYxKqhQpjtBWAyCDvZQirnAOm9KnvIHaGXIswCZ4Xbsu0Y9NL+woARPyRVQvG + # jfxy4EmO9s1s6y7OObSukwKDSNihAKHx/VIbvVWx8g2Lv5fGOa+J2Y7o9Qurs8t5 + # BQwMTt0BAoGBAPUw5Z32EszNepAeV3E2mPFUc5CLiqAxagZJuNDO2pKtyN29ETTR + # Ma4O1cWtGb6RqcNNN/Iukfkdk27Q5nC9VJSUUPYelOLc1WYOoUf6oKRzE72dkMQV + # R4bf6TkjD+OVR17fAfkswkGahZ5XA7j48KIQ+YC4jbnYKSxZTYyKPjH/AoGBAP1i + # tqXt36OVlP+y84wWqZSjMelBIVa9phDVGJmmhz3i1cMni8eLpJzWecA3pfnG6Tm9 + # ze5M4whASleEt+M00gEvNaU9ND+z0wBfi+/DwJYIbv8PQdGrBiZFrPhTPjGQUldR + # lXccV2meeLZv7TagVxSi3DO6dSJfSEHyemd5j9mBAoGAX8Hv+0gOQZQCSOTAq8Nx + # 6dZcp9gHlNaXnMsP9eTDckOSzh636JPGvj6m+GPJSSbkURUIQ3oyokMNwFqvlNos + # fTaLhAOfjBZI9WnDTTQxpugWjphJ4HqbC67JC/qIiw5S6FdaEvGLEEoD4zoChywZ + # 9oGAn+fz2d/0/JAH/FpFPgsCgYEAp/ipZgPzziiZ9ov1wbdAQcWRj7RaWnssPFpX + # jXwEiXT3CgEMO4MJ4+KWIWOChrti3qFBg6i6lDyyS6Qyls7sLFbUdC7HlTcrOEMe + # rBoTcCI1GqZNlqWOVQ65ZIEiaI7o1vPBZo2GMQEZuq8mDKFsOMThvvTrM5cAep84 + # n6HJR4ECgYABWcbsSnr0MKvVth/inxjbKapbZnp2HUCuw87Ie5zK2Of/tbC20wwk + # yKw3vrGoE3O1t1g2m2tn8UGGASeZ842jZWjIODdSi5+icysQGuULKt86h/woz2SQ + # 27GoE2i5mh6Yez6VAYbUuns3FcwIsMyWLq043Tu2DNkx9ijOOAuQzw^invalid.. + # DO NOT USE== + # -----END RSA PRIVATE KEY----- ## The lifespans configure the expiration for these token types. # access_token_lifespan: 1h diff --git a/docs/content/en/configuration/identity-providers/open-id-connect.md b/docs/content/en/configuration/identity-providers/open-id-connect.md index dde841d34..a8d6997a4 100644 --- a/docs/content/en/configuration/identity-providers/open-id-connect.md +++ b/docs/content/en/configuration/identity-providers/open-id-connect.md @@ -33,33 +33,72 @@ The following snippet provides a sample-configuration for the OIDC identity prov identity_providers: oidc: hmac_secret: this_is_a_secret_abc123abc123abc + issuer_certificate_chain: | + -----BEGIN CERTIFICATE----- + MIIC5jCCAc6gAwIBAgIRAK4Sj7FiN6PXo/urPfO4E7owDQYJKoZIhvcNAQELBQAw + EzERMA8GA1UEChMIQXV0aGVsaWEwHhcNNzAwMTAxMDAwMDAwWhcNNzEwMTAxMDAw + MDAwWjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQADggEP + ADCCAQoCggEBAPKv3pSyP4ozGEiVLJ14dIWFCEGEgq7WUMI0SZZqQA2ID0L59U/Q + /Usyy7uC9gfMUzODTpANtkOjFQcQAsxlR1FOjVBrX5QgjSvXwbQn3DtwMA7XWSl6 + LuYx2rBYSlMSN5UZQm/RxMtXfLK2b51WgEEYDFi+nECSqKzR4R54eOPkBEWRfvuY + 91AMjlhpivg8e4JWkq4LVQUKbmiFYwIdK8XQiN4blY9WwXwJFYs5sQ/UYMwBFi0H + kWOh7GEjfxgoUOPauIueZSMSlQp7zqAH39N0ZSYb6cS0Npj57QoWZSY3ak87ebcR + Nf4rCvZLby7LoN7qYCKxmCaDD3x2+NYpWH8CAwEAAaM1MDMwDgYDVR0PAQH/BAQD + AgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcN + AQELBQADggEBAHSITqIQSNzonFl3DzxHPEzr2hp6peo45buAAtu8FZHoA+U7Icfh + /ZXjPg7Xz+hgFwM/DTNGXkMWacQA/PaNWvZspgRJf2AXvNbMSs2UQODr7Tbv+Fb4 + lyblmMUNYFMCFVAMU0eIxXAFq2qcwv8UMcQFT0Z/35s6PVOakYnAGGQjTfp5Ljuq + wsdc/xWmM0cHWube6sdRRUD7SY20KU/kWzl8iFO0VbSSrDf1AlEhnLEkp1SPaxXg + OdBnl98MeoramNiJ7NT6Jnyb3zZ578fjaWfThiBpagItI8GZmG4s4Ovh2JbheN8i + ZsjNr9jqHTjhyLVbDRlmJzcqoj4JhbKs6/I^invalid DO NOT USE= + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIDBDCCAeygAwIBAgIRALJsPg21kA0zY4F1wUCIuoMwDQYJKoZIhvcNAQELBQAw + EzERMA8GA1UEChMIQXV0aGVsaWEwHhcNNzAwMTAxMDAwMDAwWhcNNzEwMTAxMDAw + MDAwWjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQADggEP + ADCCAQoCggEBAMXHBvVxUzYk0u34/DINMSF+uiOekKOAjOrC6Mi9Ww8ytPVO7t2S + zfTvM+XnEJqkFQFgimERfG/eGhjF9XIEY6LtnXe8ATvOK4nTwdufzBaoeQu3Gd50 + 5VXr6OHRo//ErrGvFXwP3g8xLePABsi/fkH3oDN+ztewOBMDzpd+KgTrk8ysv2ou + kNRMKFZZqASvCgv0LD5KWvUCnL6wgf1oTXG7aztduA4oSkUP321GpOmBC5+5ElU7 + ysoRzvD12o9QJ/IfEaulIX06w9yVMo60C/h6A3U6GdkT1SiyTIqR7v7KU/IWd/Qi + Lfftcj91VhCmJ73Meff2e2S2PrpjdXbG5FMCAwEAAaNTMFEwDgYDVR0PAQH/BAQD + AgKkMA8GA1UdJQQIMAYGBFUdJQAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU + Z7AtA3mzFc0InSBA5fiMfeLXA3owDQYJKoZIhvcNAQELBQADggEBAEE5hm1mtlk/ + kviCoHH4evbpw7rxPxDftIQlqYTtvMM4eWY/6icFoSZ4fUHEWYyps8SsPu/8f2tf + 71LGgZn0FdHi1QU2H8m0HHK7TFw+5Q6RLrLdSyk0PItJ71s9en7r8pX820nAFEHZ + HkOSfJZ7B5hFgUDkMtVM6bardXAhoqcMk4YCU96e9d4PB4eI+xGc+mNuYvov3RbB + D0s8ICyojeyPVLerz4wHjZu68Z5frAzhZ68YbzNs8j2fIBKKHkHyLG1iQyF+LJVj + 2PjCP+auJsj6fQQpMGoyGtpLcSDh+ptcTngUD8JsWipzTCjmaNqdPHAOYmcgtf4b + qocikt3WAdU^invalid DO NOT USE= + -----END CERTIFICATE----- issuer_private_key: | -----BEGIN RSA PRIVATE KEY----- - MXIEogIB$AKCAQEAxZVJP3WF//PG2fLQoEC9DtdiFG/+00vqlbVzz47nyxKONIPI - lmL3UdmqpGTKMe/5Brqse4ZAKlQHiDbwzK9ypnfigtHuvh/JO0S7ChP70RC67ed1 - HV1nyfz5eW3llbtGJPrlYLqITNgctHp6zmRUFtSzPj9qFvozI93LJi492yL1+vu8 - Un3Dm8+Qq6XM2tPdEcldB/dtBwOWoF+8eOOVsu0TDuB5bwlhBVGJuSAuzBPRS2bF - Ga4uk0JDdkDOMCEQxC5uWDFxgfERSMFyfLVWD47woDbuWEBq10c0z+dpWPMp7Ain - YnnkqicwCN88Z0zid6MmMQ65F4+9Hc+qC/p6xwIDAQABAoIBAGlhaAHKor+Su3o/ - AXqXTL5/rbYMzbLQiLt0XeJT69jpeqMTroZXHmWvXE3128mqnf0yzw/K2Ko6yxGh - i+j/onya8FqpsVYCCgfsbn2/js1AyRJeIp6Y1ORsYnqbXJnxmkXa80AV/OBPW2/+ - 60TtSdQrebY3iFPc+i2k+9bPTvpyyDLKlz8UwdZG+k5uyYNIyQTccz+PjwsIvDij - 7tKYamhhLN3QXt3/aZTFpjTgezP4WyriZxjWrddHowc47q2rwNS95ND39JcysJAc - 0Pcbu8A5lVa7Fx33uOtzDfKWIW7xVEN+OtPgN+FbTjXcXk5IZedl+pW5lU5P++G/ - ZPvz+WECgYEA9g6HwdODW3e68bOqsFoKg35+vfUFMzlyMF8HFylNVfnLpTEDr637 - owzMFvcUxVd71b+gV5nnnbI+riUFIgyR8vhCjhy4moopDPahC4/KwN4NG6uz+i1h - AB6D5+zn2BjnO/5xMMFGlApWtRNmJVGYlNDj3bXKh2VXzzy03VNeD8kCgYEAzZFL - OlzoRB1HKpTWIECcuvxofMxLOLb3zs0k2t/FYNYIpovmGWCCAULz13y53e5+/+5m - 7I9VUZJFaIhaZ36qVBApCKdru69pZMkWCcQO9jELFcx51Ez7OgJWzu7GS1QJCPKC - fEDxI0rZK21j93/Sl/nUnEir7CYpQ+wvCaGuHg8CgYAXgbncfY1+DokwkB6NbHy2 - pT4Mfbz6cNGE538w6kQ2I4AeDvmwLentYMqaow478CinegAiflSPTzkHwAemghbr - ZGZPV1UXhn13fJRUG2+eT1hnPVcbXnx223N0k8Bud6qXo65CnyRT/kzcTbcjd5Eh - Hne2daicmMTzynPo9Q72aQKBgBmobO9X8VWvIdbaxO85oVZlctVA2pK1o7CYQmVf - UM+JZ4MCKzI3rYJizPS0iK5+ujNPmmEkcs2/qBIoEsCgOrpLWhPOcc/3UPxXbPzD - D+sCrBOIdhxdj23qJNOnUfDNCGOpgUfpAzAYg4q8GKInvi1h7XukRnEvQi9MJ4LY - P1dZAoGASGcGnTMkmeSXP8ux+dvQJAiJskn/sJIgBZ5uq5GRCeLBUosRSVxM75UK - vAh/c/RBj+pYXVKuPuHGZCQJxsdcRXzXNGouUtgbaYML5Me/Hagt20QzDRBfuGBg - qeZBJaXhjElvw6PUWtg4x+LYRCBpq/bS3LK3ozZrSTukVkKDegw= + MIIEpAIBAAKCAQEA8q/elLI/ijMYSJUsnXh0hYUIQYSCrtZQwjRJlmpADYgPQvn1 + T9D9SzLLu4L2B8xTM4NOkA22Q6MVBxACzGVHUU6NUGtflCCNK9fBtCfcO3AwDtdZ + KXou5jHasFhKUxI3lRlCb9HEy1d8srZvnVaAQRgMWL6cQJKorNHhHnh44+QERZF+ + +5j3UAyOWGmK+Dx7glaSrgtVBQpuaIVjAh0rxdCI3huVj1bBfAkVizmxD9RgzAEW + LQeRY6HsYSN/GChQ49q4i55lIxKVCnvOoAff03RlJhvpxLQ2mPntChZlJjdqTzt5 + txE1/isK9ktvLsug3upgIrGYJoMPfHb41ilYfwIDAQABAoIBAQDTOdFf2JjHH1um + aPgRAvNf9v7Nj5jytaRKs5nM6iNf46ls4QPreXnMhqSeSwj6lpNgBYxOgzC9Q+cc + Y4ob/paJJPaIJTxmP8K/gyWcOQlNToL1l+eJ20eQoZm23NGr5fIsunSBwLEpTrdB + ENqqtcwhW937K8Pxy/Q1nuLyU2bc6Tn/ivLozc8n27dpQWWKh8537VY7ancIaACr + LJJLYxKqhQpjtBWAyCDvZQirnAOm9KnvIHaGXIswCZ4Xbsu0Y9NL+woARPyRVQvG + jfxy4EmO9s1s6y7OObSukwKDSNihAKHx/VIbvVWx8g2Lv5fGOa+J2Y7o9Qurs8t5 + BQwMTt0BAoGBAPUw5Z32EszNepAeV3E2mPFUc5CLiqAxagZJuNDO2pKtyN29ETTR + Ma4O1cWtGb6RqcNNN/Iukfkdk27Q5nC9VJSUUPYelOLc1WYOoUf6oKRzE72dkMQV + R4bf6TkjD+OVR17fAfkswkGahZ5XA7j48KIQ+YC4jbnYKSxZTYyKPjH/AoGBAP1i + tqXt36OVlP+y84wWqZSjMelBIVa9phDVGJmmhz3i1cMni8eLpJzWecA3pfnG6Tm9 + ze5M4whASleEt+M00gEvNaU9ND+z0wBfi+/DwJYIbv8PQdGrBiZFrPhTPjGQUldR + lXccV2meeLZv7TagVxSi3DO6dSJfSEHyemd5j9mBAoGAX8Hv+0gOQZQCSOTAq8Nx + 6dZcp9gHlNaXnMsP9eTDckOSzh636JPGvj6m+GPJSSbkURUIQ3oyokMNwFqvlNos + fTaLhAOfjBZI9WnDTTQxpugWjphJ4HqbC67JC/qIiw5S6FdaEvGLEEoD4zoChywZ + 9oGAn+fz2d/0/JAH/FpFPgsCgYEAp/ipZgPzziiZ9ov1wbdAQcWRj7RaWnssPFpX + jXwEiXT3CgEMO4MJ4+KWIWOChrti3qFBg6i6lDyyS6Qyls7sLFbUdC7HlTcrOEMe + rBoTcCI1GqZNlqWOVQ65ZIEiaI7o1vPBZo2GMQEZuq8mDKFsOMThvvTrM5cAep84 + n6HJR4ECgYABWcbsSnr0MKvVth/inxjbKapbZnp2HUCuw87Ie5zK2Of/tbC20wwk + yKw3vrGoE3O1t1g2m2tn8UGGASeZ842jZWjIODdSi5+icysQGuULKt86h/woz2SQ + 27GoE2i5mh6Yez6VAYbUuns3FcwIsMyWLq043Tu2DNkx9ijOOAuQzw^invalid.. + DO NOT USE== -----END RSA PRIVATE KEY----- access_token_lifespan: 1h authorize_code_lifespan: 1m @@ -120,6 +159,23 @@ It's __strongly recommended__ this is a [Random Alphanumeric String](../miscellaneous/guides.md#generating-a-random-alphanumeric-string) with 64 or more characters. +### issuer_certificate_chain + +{{< confkey type="string" required="no" >}} + +The certificate chain/bundle to be used with the [issuer_private_key](#issuer_private_key) DER base64 ([RFC4648]) +encoded PEM format used to sign/encrypt the [OpenID Connect] [JWT]'s. When configured it enables the [x5c] and [x5t] +JSON key's in the JWKs [Discoverable Endpoint](../../integration/openid-connect/introduction.md#discoverable-endpoints) +as per [RFC7517]. + +[RFC7517]: https://www.rfc-editor.org/rfc/rfc7517 +[x5c]: https://www.rfc-editor.org/rfc/rfc7517#section-4.7 +[x5t]: https://www.rfc-editor.org/rfc/rfc7517#section-4.8 + +The first certificate in the chain must have the public key for the [issuer_private_key](#issuer_private_key), each +certificate in the chain must be valid for the current date, and each certificate in the chain should be signed by the +certificate immediately following it if present. + ### issuer_private_key {{< confkey type="string" required="yes" >}} @@ -127,10 +183,13 @@ characters. *__Important Note:__ This can also be defined using a [secret](../methods/secrets.md) which is __strongly recommended__ especially for containerized deployments.* -The private key in DER base64 ([RFC4648]) encoded PEM format used to encrypt the [OpenID Connect] [JWT]'s. The key must -be generated by the administrator and can be done by following the +The private key in DER base64 ([RFC4648]) encoded PEM format used to sign/encrypt the [OpenID Connect] issued [JWT]'s. +The key must be generated by the administrator and can be done by following the [Generating an RSA Keypair](../miscellaneous/guides.md#generating-an-rsa-keypair) guide. +If the [issuer_certificate_chain](#issuer_certificate_chain) is provided the private key must include matching public +key data for the first certificate in the chain. + ### access_token_lifespan {{< confkey type="duration" default="1h" required="no" >}} diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index 85f1bea72..9fb6c8e72 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -815,36 +815,79 @@ notifier: ## HMAC Secret can also be set using a secret: https://www.authelia.com/c/secrets # hmac_secret: this_is_a_secret_abc123abc123abc + ## The issuer_certificate_chain is an optional PEM encoded certificate chain. It's used in conjunction with the + ## issuer_private_key to sign JWT's. All certificates in the chain must be within the validity period, and every + ## certificate included must be signed by the certificate immediately after it if provided. + # issuer_certificate_chain: | + # -----BEGIN CERTIFICATE----- + # MIIC5jCCAc6gAwIBAgIRAK4Sj7FiN6PXo/urPfO4E7owDQYJKoZIhvcNAQELBQAw + # EzERMA8GA1UEChMIQXV0aGVsaWEwHhcNNzAwMTAxMDAwMDAwWhcNNzEwMTAxMDAw + # MDAwWjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQADggEP + # ADCCAQoCggEBAPKv3pSyP4ozGEiVLJ14dIWFCEGEgq7WUMI0SZZqQA2ID0L59U/Q + # /Usyy7uC9gfMUzODTpANtkOjFQcQAsxlR1FOjVBrX5QgjSvXwbQn3DtwMA7XWSl6 + # LuYx2rBYSlMSN5UZQm/RxMtXfLK2b51WgEEYDFi+nECSqKzR4R54eOPkBEWRfvuY + # 91AMjlhpivg8e4JWkq4LVQUKbmiFYwIdK8XQiN4blY9WwXwJFYs5sQ/UYMwBFi0H + # kWOh7GEjfxgoUOPauIueZSMSlQp7zqAH39N0ZSYb6cS0Npj57QoWZSY3ak87ebcR + # Nf4rCvZLby7LoN7qYCKxmCaDD3x2+NYpWH8CAwEAAaM1MDMwDgYDVR0PAQH/BAQD + # AgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcN + # AQELBQADggEBAHSITqIQSNzonFl3DzxHPEzr2hp6peo45buAAtu8FZHoA+U7Icfh + # /ZXjPg7Xz+hgFwM/DTNGXkMWacQA/PaNWvZspgRJf2AXvNbMSs2UQODr7Tbv+Fb4 + # lyblmMUNYFMCFVAMU0eIxXAFq2qcwv8UMcQFT0Z/35s6PVOakYnAGGQjTfp5Ljuq + # wsdc/xWmM0cHWube6sdRRUD7SY20KU/kWzl8iFO0VbSSrDf1AlEhnLEkp1SPaxXg + # OdBnl98MeoramNiJ7NT6Jnyb3zZ578fjaWfThiBpagItI8GZmG4s4Ovh2JbheN8i + # ZsjNr9jqHTjhyLVbDRlmJzcqoj4JhbKs6/I^invalid DO NOT USE= + # -----END CERTIFICATE----- + # -----BEGIN CERTIFICATE----- + # MIIDBDCCAeygAwIBAgIRALJsPg21kA0zY4F1wUCIuoMwDQYJKoZIhvcNAQELBQAw + # EzERMA8GA1UEChMIQXV0aGVsaWEwHhcNNzAwMTAxMDAwMDAwWhcNNzEwMTAxMDAw + # MDAwWjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQADggEP + # ADCCAQoCggEBAMXHBvVxUzYk0u34/DINMSF+uiOekKOAjOrC6Mi9Ww8ytPVO7t2S + # zfTvM+XnEJqkFQFgimERfG/eGhjF9XIEY6LtnXe8ATvOK4nTwdufzBaoeQu3Gd50 + # 5VXr6OHRo//ErrGvFXwP3g8xLePABsi/fkH3oDN+ztewOBMDzpd+KgTrk8ysv2ou + # kNRMKFZZqASvCgv0LD5KWvUCnL6wgf1oTXG7aztduA4oSkUP321GpOmBC5+5ElU7 + # ysoRzvD12o9QJ/IfEaulIX06w9yVMo60C/h6A3U6GdkT1SiyTIqR7v7KU/IWd/Qi + # Lfftcj91VhCmJ73Meff2e2S2PrpjdXbG5FMCAwEAAaNTMFEwDgYDVR0PAQH/BAQD + # AgKkMA8GA1UdJQQIMAYGBFUdJQAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU + # Z7AtA3mzFc0InSBA5fiMfeLXA3owDQYJKoZIhvcNAQELBQADggEBAEE5hm1mtlk/ + # kviCoHH4evbpw7rxPxDftIQlqYTtvMM4eWY/6icFoSZ4fUHEWYyps8SsPu/8f2tf + # 71LGgZn0FdHi1QU2H8m0HHK7TFw+5Q6RLrLdSyk0PItJ71s9en7r8pX820nAFEHZ + # HkOSfJZ7B5hFgUDkMtVM6bardXAhoqcMk4YCU96e9d4PB4eI+xGc+mNuYvov3RbB + # D0s8ICyojeyPVLerz4wHjZu68Z5frAzhZ68YbzNs8j2fIBKKHkHyLG1iQyF+LJVj + # 2PjCP+auJsj6fQQpMGoyGtpLcSDh+ptcTngUD8JsWipzTCjmaNqdPHAOYmcgtf4b + # qocikt3WAdU^invalid DO NOT USE= + # -----END CERTIFICATE----- + ## The issuer_private_key is used to sign the JWT forged by OpenID Connect. ## Issuer Private Key can also be set using a secret: https://www.authelia.com/c/secrets # issuer_private_key: | # -----BEGIN RSA PRIVATE KEY----- - # MXIEogIB$AKCAQEAxZVJP3WF//PG2fLQoEC9DtdiFG/+00vqlbVzz47nyxKONIPI - # lmL3UdmqpGTKMe/5Brqse4ZAKlQHiDbwzK9ypnfigtHuvh/JO0S7ChP70RC67ed1 - # HV1nyfz5eW3llbtGJPrlYLqITNgctHp6zmRUFtSzPj9qFvozI93LJi492yL1+vu8 - # Un3Dm8+Qq6XM2tPdEcldB/dtBwOWoF+8eOOVsu0TDuB5bwlhBVGJuSAuzBPRS2bF - # Ga4uk0JDdkDOMCEQxC5uWDFxgfERSMFyfLVWD47woDbuWEBq10c0z+dpWPMp7Ain - # YnnkqicwCN88Z0zid6MmMQ65F4+9Hc+qC/p6xwIDAQABAoIBAGlhaAHKor+Su3o/ - # AXqXTL5/rbYMzbLQiLt0XeJT69jpeqMTroZXHmWvXE3128mqnf0yzw/K2Ko6yxGh - # i+j/onya8FqpsVYCCgfsbn2/js1AyRJeIp6Y1ORsYnqbXJnxmkXa80AV/OBPW2/+ - # 60TtSdQrebY3iFPc+i2k+9bPTvpyyDLKlz8UwdZG+k5uyYNIyQTccz+PjwsIvDij - # 7tKYamhhLN3QXt3/aZTFpjTgezP4WyriZxjWrddHowc47q2rwNS95ND39JcysJAc - # 0Pcbu8A5lVa7Fx33uOtzDfKWIW7xVEN+OtPgN+FbTjXcXk5IZedl+pW5lU5P++G/ - # ZPvz+WECgYEA9g6HwdODW3e68bOqsFoKg35+vfUFMzlyMF8HFylNVfnLpTEDr637 - # owzMFvcUxVd71b+gV5nnnbI+riUFIgyR8vhCjhy4moopDPahC4/KwN4NG6uz+i1h - # AB6D5+zn2BjnO/5xMMFGlApWtRNmJVGYlNDj3bXKh2VXzzy03VNeD8kCgYEAzZFL - # OlzoRB1HKpTWIECcuvxofMxLOLb3zs0k2t/FYNYIpovmGWCCAULz13y53e5+/+5m - # 7I9VUZJFaIhaZ36qVBApCKdru69pZMkWCcQO9jELFcx51Ez7OgJWzu7GS1QJCPKC - # fEDxI0rZK21j93/Sl/nUnEir7CYpQ+wvCaGuHg8CgYAXgbncfY1+DokwkB6NbHy2 - # pT4Mfbz6cNGE538w6kQ2I4AeDvmwLentYMqaow478CinegAiflSPTzkHwAemghbr - # ZGZPV1UXhn13fJRUG2+eT1hnPVcbXnx223N0k8Bud6qXo65CnyRT/kzcTbcjd5Eh - # Hne2daicmMTzynPo9Q72aQKBgBmobO9X8VWvIdbaxO85oVZlctVA2pK1o7CYQmVf - # UM+JZ4MCKzI3rYJizPS0iK5+ujNPmmEkcs2/qBIoEsCgOrpLWhPOcc/3UPxXbPzD - # D+sCrBOIdhxdj23qJNOnUfDNCGOpgUfpAzAYg4q8GKInvi1h7XukRnEvQi9MJ4LY - # P1dZAoGASGcGnTMkmeSXP8ux+dvQJAiJskn/sJIgBZ5uq5GRCeLBUosRSVxM75UK - # vAh/c/RBj+pYXVKuPuHGZCQJxsdcRXzXNGouUtgbaYML5Me/Hagt20QzDRBfuGBg - # qeZBJaXhjElvw6PUWtg4x+LYRCBpq/bS3LK3ozZrSTukVkKDegw= - # -----END RSA PRIVATE KEY----- + # MIIEpAIBAAKCAQEA8q/elLI/ijMYSJUsnXh0hYUIQYSCrtZQwjRJlmpADYgPQvn1 + # T9D9SzLLu4L2B8xTM4NOkA22Q6MVBxACzGVHUU6NUGtflCCNK9fBtCfcO3AwDtdZ + # KXou5jHasFhKUxI3lRlCb9HEy1d8srZvnVaAQRgMWL6cQJKorNHhHnh44+QERZF+ + # +5j3UAyOWGmK+Dx7glaSrgtVBQpuaIVjAh0rxdCI3huVj1bBfAkVizmxD9RgzAEW + # LQeRY6HsYSN/GChQ49q4i55lIxKVCnvOoAff03RlJhvpxLQ2mPntChZlJjdqTzt5 + # txE1/isK9ktvLsug3upgIrGYJoMPfHb41ilYfwIDAQABAoIBAQDTOdFf2JjHH1um + # aPgRAvNf9v7Nj5jytaRKs5nM6iNf46ls4QPreXnMhqSeSwj6lpNgBYxOgzC9Q+cc + # Y4ob/paJJPaIJTxmP8K/gyWcOQlNToL1l+eJ20eQoZm23NGr5fIsunSBwLEpTrdB + # ENqqtcwhW937K8Pxy/Q1nuLyU2bc6Tn/ivLozc8n27dpQWWKh8537VY7ancIaACr + # LJJLYxKqhQpjtBWAyCDvZQirnAOm9KnvIHaGXIswCZ4Xbsu0Y9NL+woARPyRVQvG + # jfxy4EmO9s1s6y7OObSukwKDSNihAKHx/VIbvVWx8g2Lv5fGOa+J2Y7o9Qurs8t5 + # BQwMTt0BAoGBAPUw5Z32EszNepAeV3E2mPFUc5CLiqAxagZJuNDO2pKtyN29ETTR + # Ma4O1cWtGb6RqcNNN/Iukfkdk27Q5nC9VJSUUPYelOLc1WYOoUf6oKRzE72dkMQV + # R4bf6TkjD+OVR17fAfkswkGahZ5XA7j48KIQ+YC4jbnYKSxZTYyKPjH/AoGBAP1i + # tqXt36OVlP+y84wWqZSjMelBIVa9phDVGJmmhz3i1cMni8eLpJzWecA3pfnG6Tm9 + # ze5M4whASleEt+M00gEvNaU9ND+z0wBfi+/DwJYIbv8PQdGrBiZFrPhTPjGQUldR + # lXccV2meeLZv7TagVxSi3DO6dSJfSEHyemd5j9mBAoGAX8Hv+0gOQZQCSOTAq8Nx + # 6dZcp9gHlNaXnMsP9eTDckOSzh636JPGvj6m+GPJSSbkURUIQ3oyokMNwFqvlNos + # fTaLhAOfjBZI9WnDTTQxpugWjphJ4HqbC67JC/qIiw5S6FdaEvGLEEoD4zoChywZ + # 9oGAn+fz2d/0/JAH/FpFPgsCgYEAp/ipZgPzziiZ9ov1wbdAQcWRj7RaWnssPFpX + # jXwEiXT3CgEMO4MJ4+KWIWOChrti3qFBg6i6lDyyS6Qyls7sLFbUdC7HlTcrOEMe + # rBoTcCI1GqZNlqWOVQ65ZIEiaI7o1vPBZo2GMQEZuq8mDKFsOMThvvTrM5cAep84 + # n6HJR4ECgYABWcbsSnr0MKvVth/inxjbKapbZnp2HUCuw87Ie5zK2Of/tbC20wwk + # yKw3vrGoE3O1t1g2m2tn8UGGASeZ842jZWjIODdSi5+icysQGuULKt86h/woz2SQ + # 27GoE2i5mh6Yez6VAYbUuns3FcwIsMyWLq043Tu2DNkx9ijOOAuQzw^invalid.. + # DO NOT USE== + # -----END RSA PRIVATE KEY----- ## The lifespans configure the expiration for these token types. # access_token_lifespan: 1h diff --git a/internal/configuration/const.go b/internal/configuration/const.go index 097da815c..2f42563e0 100644 --- a/internal/configuration/const.go +++ b/internal/configuration/const.go @@ -31,8 +31,9 @@ const ( errFmtSecretIOIssue = "secrets: error loading secret path %s into key '%s': %v" errFmtGenerateConfiguration = "error occurred generating configuration: %+v" - errFmtDecodeHookCouldNotParse = "could not decode '%s' to a %s: %w" - errFmtDecodeHookCouldNotParseEmptyValue = "could not decode an empty value to a %s: %w" + errFmtDecodeHookCouldNotParse = "could not decode '%s' to a %s%s: %w" + errFmtDecodeHookCouldNotParseBasic = "could not decode to a %s%s: %w" + errFmtDecodeHookCouldNotParseEmptyValue = "could not decode an empty value to a %s%s: %w" ) var secretSuffixes = []string{"key", "secret", "password", "token"} diff --git a/internal/configuration/decode_hooks.go b/internal/configuration/decode_hooks.go index 406933f96..31f657f37 100644 --- a/internal/configuration/decode_hooks.go +++ b/internal/configuration/decode_hooks.go @@ -1,6 +1,8 @@ package configuration import ( + "crypto/rsa" + "crypto/x509" "fmt" "net/mail" "net/url" @@ -23,11 +25,11 @@ func StringToMailAddressHookFunc() mapstructure.DecodeHookFuncType { return data, nil } - kindStr := "mail.Address (RFC5322)" + prefixType := "" if t.Kind() == reflect.Ptr { ptr = true - kindStr = "*" + kindStr + prefixType = "*" } expectedType := reflect.TypeOf(mail.Address{}) @@ -44,7 +46,7 @@ func StringToMailAddressHookFunc() mapstructure.DecodeHookFuncType { if dataStr != "" { if result, err = mail.ParseAddress(dataStr); err != nil { - return nil, fmt.Errorf(errFmtDecodeHookCouldNotParse, dataStr, kindStr, err) + return nil, fmt.Errorf(errFmtDecodeHookCouldNotParse, dataStr, prefixType, expectedType.String()+" (RFC5322)", err) } } @@ -69,11 +71,11 @@ func StringToURLHookFunc() mapstructure.DecodeHookFuncType { return data, nil } - kindStr := "url.URL" + prefixType := "" if t.Kind() == reflect.Ptr { ptr = true - kindStr = "*" + kindStr + prefixType = "*" } expectedType := reflect.TypeOf(url.URL{}) @@ -90,7 +92,7 @@ func StringToURLHookFunc() mapstructure.DecodeHookFuncType { if dataStr != "" { if result, err = url.Parse(dataStr); err != nil { - return nil, fmt.Errorf(errFmtDecodeHookCouldNotParse, dataStr, kindStr, err) + return nil, fmt.Errorf(errFmtDecodeHookCouldNotParse, dataStr, prefixType, expectedType, err) } } @@ -119,11 +121,11 @@ func ToTimeDurationHookFunc() mapstructure.DecodeHookFuncType { return data, nil } - kindStr := "time.Duration" + prefixType := "" if t.Kind() == reflect.Ptr { ptr = true - kindStr = "*" + kindStr + prefixType = "*" } expectedType := reflect.TypeOf(time.Duration(0)) @@ -141,7 +143,7 @@ func ToTimeDurationHookFunc() mapstructure.DecodeHookFuncType { dataStr := data.(string) if result, err = utils.ParseDurationString(dataStr); err != nil { - return nil, fmt.Errorf(errFmtDecodeHookCouldNotParse, dataStr, kindStr, err) + return nil, fmt.Errorf(errFmtDecodeHookCouldNotParse, dataStr, prefixType, expectedType, err) } case f.Kind() == reflect.Int: seconds := data.(int) @@ -176,11 +178,11 @@ func StringToRegexpHookFunc() mapstructure.DecodeHookFuncType { return data, nil } - kindStr := "regexp.Regexp" + prefixType := "" if t.Kind() == reflect.Ptr { ptr = true - kindStr = "*" + kindStr + prefixType = "*" } expectedType := reflect.TypeOf(regexp.Regexp{}) @@ -197,7 +199,7 @@ func StringToRegexpHookFunc() mapstructure.DecodeHookFuncType { if dataStr != "" { if result, err = regexp.Compile(dataStr); err != nil { - return nil, fmt.Errorf(errFmtDecodeHookCouldNotParse, dataStr, kindStr, err) + return nil, fmt.Errorf(errFmtDecodeHookCouldNotParse, dataStr, prefixType, expectedType, err) } } @@ -206,7 +208,7 @@ func StringToRegexpHookFunc() mapstructure.DecodeHookFuncType { } if result == nil { - return nil, fmt.Errorf(errFmtDecodeHookCouldNotParseEmptyValue, kindStr, errDecodeNonPtrMustHaveValue) + return nil, fmt.Errorf(errFmtDecodeHookCouldNotParseEmptyValue, prefixType, expectedType, errDecodeNonPtrMustHaveValue) } return *result, nil @@ -222,11 +224,11 @@ func StringToAddressHookFunc() mapstructure.DecodeHookFuncType { return data, nil } - kindStr := "Address" + prefixType := "" if t.Kind() == reflect.Ptr { ptr = true - kindStr = "*" + kindStr + prefixType = "*" } expectedType := reflect.TypeOf(schema.Address{}) @@ -242,7 +244,7 @@ func StringToAddressHookFunc() mapstructure.DecodeHookFuncType { var result *schema.Address if result, err = schema.NewAddressFromString(dataStr); err != nil { - return nil, fmt.Errorf(errFmtDecodeHookCouldNotParse, dataStr, kindStr, err) + return nil, fmt.Errorf(errFmtDecodeHookCouldNotParse, dataStr, prefixType, expectedType, err) } if ptr { @@ -252,3 +254,131 @@ func StringToAddressHookFunc() mapstructure.DecodeHookFuncType { return *result, nil } } + +// StringToX509CertificateHookFunc decodes strings to x509.Certificate's. +func StringToX509CertificateHookFunc() mapstructure.DecodeHookFuncType { + return func(f reflect.Type, t reflect.Type, data interface{}) (value interface{}, err error) { + if f.Kind() != reflect.String { + return data, nil + } + + if t.Kind() != reflect.Ptr { + return data, nil + } + + expectedType := reflect.TypeOf(x509.Certificate{}) + + if t.Elem() != expectedType { + return data, nil + } + + dataStr := data.(string) + + var result *x509.Certificate + + if dataStr == "" { + return result, nil + } + + var i interface{} + + if i, err = utils.ParseX509FromPEM([]byte(dataStr)); err != nil { + return nil, fmt.Errorf(errFmtDecodeHookCouldNotParseBasic, "*", expectedType, err) + } + + switch r := i.(type) { + case *x509.Certificate: + return r, nil + default: + return nil, fmt.Errorf(errFmtDecodeHookCouldNotParseBasic, "*", expectedType, fmt.Errorf("the data is for a %T not a *%s", r, expectedType)) + } + } +} + +// StringToX509CertificateChainHookFunc decodes strings to schema.X509CertificateChain's. +func StringToX509CertificateChainHookFunc() mapstructure.DecodeHookFuncType { + return func(f reflect.Type, t reflect.Type, data interface{}) (value interface{}, err error) { + var ptr bool + + if f.Kind() != reflect.String { + return data, nil + } + + prefixType := "" + + if t.Kind() == reflect.Ptr { + ptr = true + prefixType = "*" + } + + expectedType := reflect.TypeOf(schema.X509CertificateChain{}) + + if ptr && t.Elem() != expectedType { + return data, nil + } else if !ptr && t != expectedType { + return data, nil + } + + dataStr := data.(string) + + var result *schema.X509CertificateChain + + if dataStr == "" && ptr { + return result, nil + } + + if result, err = schema.NewX509CertificateChain(dataStr); err != nil { + return nil, fmt.Errorf(errFmtDecodeHookCouldNotParseBasic, prefixType, expectedType, err) + } + + if ptr { + return result, nil + } + + if result == nil { + return schema.X509CertificateChain{}, nil + } + + return *result, nil + } +} + +// StringToRSAPrivateKeyHookFunc decodes strings to rsa.PrivateKey's. +func StringToRSAPrivateKeyHookFunc() mapstructure.DecodeHookFuncType { + return func(f reflect.Type, t reflect.Type, data interface{}) (value interface{}, err error) { + if f.Kind() != reflect.String { + return data, nil + } + + if t.Kind() != reflect.Ptr { + return data, nil + } + + expectedType := reflect.TypeOf(rsa.PrivateKey{}) + + if t.Elem() != expectedType { + return data, nil + } + + dataStr := data.(string) + + var result *rsa.PrivateKey + + if dataStr == "" { + return result, nil + } + + var i interface{} + + if i, err = utils.ParseX509FromPEM([]byte(dataStr)); err != nil { + return nil, fmt.Errorf(errFmtDecodeHookCouldNotParseBasic, "*", expectedType, err) + } + + switch r := i.(type) { + case *rsa.PrivateKey: + return r, nil + default: + return nil, fmt.Errorf(errFmtDecodeHookCouldNotParseBasic, "*", expectedType, fmt.Errorf("the data is for a %T not a *%s", r, expectedType)) + } + } +} diff --git a/internal/configuration/decode_hooks_test.go b/internal/configuration/decode_hooks_test.go index cbf653394..69e947de0 100644 --- a/internal/configuration/decode_hooks_test.go +++ b/internal/configuration/decode_hooks_test.go @@ -1,6 +1,11 @@ package configuration_test import ( + "bytes" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "net/mail" "net/url" "reflect" @@ -833,7 +838,7 @@ func TestStringToAddressHookFunc(t *testing.T) { name: "ShouldFailDecode", have: "tcp://&!@^#*&!@#&*@!:2020", expected: schema.Address{}, - err: "could not decode 'tcp://&!@^#*&!@#&*@!:2020' to a Address: could not parse string 'tcp://&!@^#*&!@#&*@!:2020' as address: expected format is [://][:]: parse \"tcp://&!@^\": invalid character \"^\" in host name", + err: "could not decode 'tcp://&!@^#*&!@#&*@!:2020' to a schema.Address: could not parse string 'tcp://&!@^#*&!@#&*@!:2020' as address: expected format is [://][:]: parse \"tcp://&!@^\": invalid character \"^\" in host name", decode: false, }, } @@ -862,6 +867,426 @@ func TestStringToAddressHookFunc(t *testing.T) { } } +func TestStringToRSAPrivateKeyHookFunc(t *testing.T) { + var nilkey *rsa.PrivateKey + + testCases := []struct { + desc string + have interface{} + want interface{} + err string + decode bool + }{ + { + desc: "ShouldDecodeRSAPrivateKey", + have: x509PrivateKeyRSA1, + want: mustParseRSAPrivateKey(x509PrivateKeyRSA1), + decode: true, + }, + { + desc: "ShouldNotDecodeToECDSAPrivateKey", + have: x509PrivateKeyRSA1, + want: &ecdsa.PrivateKey{}, + decode: false, + }, + { + desc: "ShouldNotDecodeEmptyKey", + have: "", + want: nilkey, + decode: true, + }, + { + desc: "ShouldNotDecodeECDSAKeyToRSAKey", + have: x509PrivateKeyEC1, + want: nilkey, + decode: true, + err: "could not decode to a *rsa.PrivateKey: the data is for a *ecdsa.PrivateKey not a *rsa.PrivateKey", + }, + { + desc: "ShouldNotDecodeBadRSAPrivateKey", + have: x509PrivateKeyRSA2, + want: nilkey, + decode: true, + err: "could not decode to a *rsa.PrivateKey: failed to parse PEM block containing the key", + }, + } + + hook := configuration.StringToRSAPrivateKeyHookFunc() + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + result, err := hook(reflect.TypeOf(tc.have), reflect.TypeOf(tc.want), tc.have) + switch { + case !tc.decode: + assert.NoError(t, err) + assert.Equal(t, tc.have, result) + case tc.err == "": + assert.NoError(t, err) + require.Equal(t, tc.want, result) + default: + assert.EqualError(t, err, tc.err) + assert.Nil(t, result) + } + }) + } +} + +func TestStringToX509CertificateHookFunc(t *testing.T) { + var nilkey *x509.Certificate + + testCases := []struct { + desc string + have interface{} + want interface{} + err string + decode bool + }{ + { + desc: "ShouldDecodeRSACertificate", + have: x509CertificateRSA1, + want: mustParseX509Certificate(x509CertificateRSA1), + decode: true, + }, + { + desc: "ShouldDecodeECDSACertificate", + have: x509CACertificateECDSA, + want: mustParseX509Certificate(x509CACertificateECDSA), + decode: true, + }, + { + desc: "ShouldDecodeRSACACertificate", + have: x509CACertificateRSA, + want: mustParseX509Certificate(x509CACertificateRSA), + decode: true, + }, + { + desc: "ShouldDecodeECDSACACertificate", + have: x509CACertificateECDSA, + want: mustParseX509Certificate(x509CACertificateECDSA), + decode: true, + }, + { + desc: "ShouldDecodeEmptyCertificateToNil", + have: "", + want: nilkey, + decode: true, + }, + { + desc: "ShouldNotDecodeECDSAKeyToCertificate", + have: x509PrivateKeyEC1, + want: nilkey, + decode: true, + err: "could not decode to a *x509.Certificate: the data is for a *ecdsa.PrivateKey not a *x509.Certificate", + }, + { + desc: "ShouldNotDecodeBadRSAPrivateKeyToCertificate", + have: x509PrivateKeyRSA2, + want: nilkey, + decode: true, + err: "could not decode to a *x509.Certificate: failed to parse PEM block containing the key", + }, + } + + hook := configuration.StringToX509CertificateHookFunc() + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + result, err := hook(reflect.TypeOf(tc.have), reflect.TypeOf(tc.want), tc.have) + switch { + case !tc.decode: + assert.NoError(t, err) + assert.Equal(t, tc.have, result) + case tc.err == "": + assert.NoError(t, err) + require.Equal(t, tc.want, result) + default: + assert.EqualError(t, err, tc.err) + assert.Nil(t, result) + } + }) + } +} + +func TestStringToX509CertificateChainHookFunc(t *testing.T) { + var nilkey *schema.X509CertificateChain + + testCases := []struct { + desc string + have interface{} + expected interface{} + err, verr string + decode bool + }{ + { + desc: "ShouldDecodeRSACertificate", + have: x509CertificateRSA1, + expected: mustParseX509CertificateChain(x509CertificateRSA1), + decode: true, + }, + { + desc: "ShouldDecodeRSACertificateNoPtr", + have: x509CertificateRSA1, + expected: *mustParseX509CertificateChain(x509CertificateRSA1), + decode: true, + }, + { + desc: "ShouldDecodeRSACertificateChain", + have: buildChain(x509CertificateRSA1, x509CACertificateRSA), + expected: mustParseX509CertificateChain(x509CertificateRSA1, x509CACertificateRSA), + decode: true, + }, + { + desc: "ShouldDecodeRSACertificateChainNoPtr", + have: buildChain(x509CertificateRSA1, x509CACertificateRSA), + expected: *mustParseX509CertificateChain(x509CertificateRSA1, x509CACertificateRSA), + decode: true, + }, + { + desc: "ShouldNotDecodeBadRSACertificateChain", + have: buildChain(x509CertificateRSA1, x509CACertificateECDSA), + expected: mustParseX509CertificateChain(x509CertificateRSA1, x509CACertificateECDSA), + verr: "certificate #1 in chain is not signed properly by certificate #2 in chain: x509: signature algorithm specifies an RSA public key, but have public key of type *ecdsa.PublicKey", + decode: true, + }, + { + desc: "ShouldDecodeECDSACertificate", + have: x509CACertificateECDSA, + expected: mustParseX509CertificateChain(x509CACertificateECDSA), + decode: true, + }, + { + desc: "ShouldDecodeRSACACertificate", + have: x509CACertificateRSA, + expected: mustParseX509CertificateChain(x509CACertificateRSA), + decode: true, + }, + { + desc: "ShouldDecodeECDSACACertificate", + have: x509CACertificateECDSA, + expected: mustParseX509CertificateChain(x509CACertificateECDSA), + decode: true, + }, + { + desc: "ShouldDecodeEmptyCertificateToNil", + have: "", + expected: nilkey, + decode: true, + }, + { + desc: "ShouldDecodeEmptyCertificateToEmptyStruct", + have: "", + expected: schema.X509CertificateChain{}, + decode: true, + }, + { + desc: "ShouldNotDecodeECDSAKeyToCertificate", + have: x509PrivateKeyEC1, + expected: nilkey, + decode: true, + err: "could not decode to a *schema.X509CertificateChain: the PEM data chain contains a EC PRIVATE KEY but only certificates are expected", + }, + { + desc: "ShouldNotDecodeBadRSAPrivateKeyToCertificate", + have: x509PrivateKeyRSA2, + expected: nilkey, + decode: true, + err: "could not decode to a *schema.X509CertificateChain: invalid PEM block", + }, + } + + hook := configuration.StringToX509CertificateChainHookFunc() + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + actual, err := hook(reflect.TypeOf(tc.have), reflect.TypeOf(tc.expected), tc.have) + switch { + case !tc.decode: + assert.NoError(t, err) + assert.Equal(t, tc.have, actual) + case tc.err == "": + assert.NoError(t, err) + require.Equal(t, tc.expected, actual) + + if tc.expected == nilkey { + break + } + + switch chain := actual.(type) { + case *schema.X509CertificateChain: + require.NotNil(t, chain) + if tc.verr == "" { + assert.NoError(t, chain.Validate()) + } else { + assert.EqualError(t, chain.Validate(), tc.verr) + } + case schema.X509CertificateChain: + require.NotNil(t, chain) + if tc.verr == "" { + assert.NoError(t, chain.Validate()) + } else { + assert.EqualError(t, chain.Validate(), tc.verr) + } + } + default: + assert.EqualError(t, err, tc.err) + assert.Nil(t, actual) + } + }) + } +} + +var ( + x509PrivateKeyRSA1 = ` +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA6z1LOg1ZCqb0lytXWZ+MRBpMHEXOoTOLYgfZXt1IYyE3Z758 +cyalk0NYQhY5cZDsXPYWPvAHiPMUxutWkoxFwby56S+AbIMa3/Is+ILrHRJs8Exn +ZkpyrYFxPX12app2kErdmAkHSx0Z5/kuXiz96PHs8S8/ZbyZolLHzdfLtSzjvRm5 +Zue5iFzsf19NJz5CIBfv8g5lRwtE8wNJoRSpn1xq7fqfuA0weDNFPzjlNWRLy6aa +rK7qJexRkmkCs4sLgyl+9NODYJpvmN8E1yhyC27E0joI6rBFVW7Ihv+cSPCdDzGp +EWe81x3AeqAa3mjVqkiq4u4Z2i8JDgBaPboqJwIDAQABAoIBAAFdLZ58jVOefDSU +L8F5R1rtvBs93GDa56f926jNJ6pLewLC+/2+757W+SAI+PRLntM7Kg3bXm/Q2QH+ +Q1Y+MflZmspbWCdI61L5GIGoYKyeers59i+FpvySj5GHtLQRiTZ0+Kv1AXHSDWBm +9XneUOqU3IbZe0ifu1RRno72/VtjkGXbW8Mkkw+ohyGbIeTx/0/JQ6sSNZTT3Vk7 +8i4IXptq3HSF0/vqZuah8rShoeNq72pD1YLM9YPdL5by1QkDLnqATDiCpLBTCaNV +I8sqYEun+HYbQzBj8ZACG2JVZpEEidONWQHw5BPWO95DSZYrVnEkuCqeH+u5vYt7 +CHuJ3AECgYEA+W3v5z+j91w1VPHS0VB3SCDMouycAMIUnJPAbt+0LPP0scUFsBGE +hPAKddC54pmMZRQ2KIwBKiyWfCrJ8Xz8Yogn7fJgmwTHidJBr2WQpIEkNGlK3Dzi +jXL2sh0yC7sHvn0DqiQ79l/e7yRbSnv2wrTJEczOOH2haD7/tBRyCYECgYEA8W+q +E9YyGvEltnPFaOxofNZ8LHVcZSsQI5b6fc0iE7fjxFqeXPXEwGSOTwqQLQRiHn9b +CfPmIG4Vhyq0otVmlPvUnfBZ2OK+tl5X2/mQFO3ROMdvpi0KYa994uqfJdSTaqLn +jjoKFB906UFHnDQDLZUNiV1WwnkTglgLc+xrd6cCgYEAqqthyv6NyBTM3Tm2gcio +Ra9Dtntl51LlXZnvwy3IkDXBCd6BHM9vuLKyxZiziGx+Vy90O1xI872cnot8sINQ +Am+dur/tAEVN72zxyv0Y8qb2yfH96iKy9gxi5s75TnOEQgAygLnYWaWR2lorKRUX +bHTdXBOiS58S0UzCFEslGIECgYBqkO4SKWYeTDhoKvuEj2yjRYyzlu28XeCWxOo1 +otiauX0YSyNBRt2cSgYiTzhKFng0m+QUJYp63/wymB/5C5Zmxi0XtWIDADpLhqLj +HmmBQ2Mo26alQ5YkffBju0mZyhVzaQop1eZi8WuKFV1FThPlB7hc3E0SM5zv2Grd +tQnOWwKBgQC40yZY0PcjuILhy+sIc0Wvh7LUA7taSdTye149kRvbvsCDN7Jh75lM +USjhLXY0Nld2zBm9r8wMb81mXH29uvD+tDqqsICvyuKlA/tyzXR+QTr7dCVKVwu0 +1YjCJ36UpTsLre2f8nOSLtNmRfDPtbOE2mkOoO9dD9UU0XZwnvn9xw== +-----END RSA PRIVATE KEY-----` + + x509PrivateKeyRSA2 = ` +-----BEGIN RSA PRIVATE KEY----- +bad key +-----END RSA PRIVATE KEY-----` + + x509PrivateKeyEC1 = ` +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMn970LSn8aKVhBM4vyUmpZyEdCT4riN+Lp4QU04zUhYoAoGCCqGSM49 +AwEHoUQDQgAEMD69n22nd78GmaRDzy/s7muqhbc/OEnFS2mNtiRAA5FaX+kbkCB5 +8pu/k2jkaSVNZtBYKPVAibHkhvakjVb66A== +-----END EC PRIVATE KEY-----` + + x509CertificateRSA1 = ` +-----BEGIN CERTIFICATE----- +MIIC5TCCAc2gAwIBAgIQfBUmKLmEvMqS6S9auKCY2DANBgkqhkiG9w0BAQsFADAT +MREwDwYDVQQKEwhBdXRoZWxpYTAeFw0yMjA5MDgxMDA5MThaFw0yMzA5MDgxMDA5 +MThaMBMxETAPBgNVBAoTCEF1dGhlbGlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEApqno1cOpDcgKmOJqeDQGIGH5/ZnqcJ4xud6eOUfbDqel3b0RkAQX +mFYWEDO/PDOAOjYk/xSwZGo3jDofOHGhrKstQqLdweHGfme5NXYHJda7nGv/OY5q +zUuEG4xBVgUsvbshWZ18H+bIQpwiP6tDAabxc0B7J15F1pArK8QN4pDTfsqZDwMi +Qyo638XfUbDzEVZRbdDKxHz5g0w2vFdXon8uOxRRb0+zlHF9nM4PiESNgiUIYeua +8Q5yP10SY2k9zlQ/OFJ4XhQmioCJvNjJE/TSc5/ECub2n7hTZhN5TGKagukZ5NAy +KgbvNYW+CN+H4pFJt/9WptiDfBqhlUvjnwIDAQABozUwMzAOBgNVHQ8BAf8EBAMC +BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0B +AQsFAAOCAQEAH9veGzfWqXxsa5s2KHV2Jzed9V8KSs1Qy9QKRez1i2OMvMPh2DRM +RLzAAp/XigjxLQF/LFXuoFW0Qg8BRb44iRgZrCiqVOUnd3xTrS/CcFExnpQI4F12 +/U70o97rkTonCOHmUUW6vQfWSXR/GU3/faRLJjiqcpWLZhTQNrnsip1ym3B2NMdk +gMKkT8Acx1DX48MvTE4+DyqCS8TlJbacBJ2RFFELKu3jYnVNyrb0ywLxoCtWqBBE +veVj+VMn9hNY1u5uydLsUDOlT5QyQcEuUzjjdhsJKEgDE5daNtB2OJJnd9IOMzUA +hasPZETCCKabTpWiEPw1Cn/ZRqya0SZqFg== +-----END CERTIFICATE----- +` + + x509CACertificateRSA = ` +-----BEGIN CERTIFICATE----- +MIIDBDCCAeygAwIBAgIRAJfz0dHS9UkDngE55lUPdu4wDQYJKoZIhvcNAQELBQAw +EzERMA8GA1UEChMIQXV0aGVsaWEwHhcNMjIwOTA4MDk1OTI1WhcNMjMwOTA4MDk1 +OTI1WjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALfivbwq9r5X8N+NSbNHVuKbCb9f9vD5Xw2pOjSVvVjFkWQ1YKJu +JGx9yskhHBZTBt76cInipA+0PqCBrBrjij1lh2StvzRVuQwgFG6H01LxBPi0JyYv +Is94F6PHr6fSBgFWB5GNQ797KQIOdIr057uEFbp0eBMxxqiQ9gdyD0HPretrx1Uy +kHuF6jck958combn9luHW0i53mt8706j7UAhxFqu9YUeklTM1VqUiRm5+nJKIdNA +LiDMGVAuoxjhF6aIgY0yh5mL5mKtYYzhtA8WryrMzBgFRUGzHCSI1TNisA8wSf2T +Z2JhbFHrFPR5fiSqAEHok3UXu++wsfl/lisCAwEAAaNTMFEwDgYDVR0PAQH/BAQD +AgKkMA8GA1UdJQQIMAYGBFUdJQAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +OSXG42bCPNuWeP0ahScUMVjxe/wwDQYJKoZIhvcNAQELBQADggEBAFRnubHiYy1H +PGODKA++UY9eAmmaCJxzuWpY8FY9fBz8/VBzcp8vaURPmmQ/34QcqfaSHDM2jIaL +dQ2o9Ae5NjbRzLB6a5DcVO50oHG4BHP1ix4Bt3POr8J80KgA9pOIyAQqbAlFBSzQ +l9yrzVULyf+qpUmByRf5qy2kQJOBfMJbn5j+BprWKwbcI8OAZWWSLItTXqJDrFTk +OMZK4wZ6KiZM07KWMlwW/CE0QRzDk5MXfbwRt4D8pyx6rGKqI7QRusjm5osIpHZV +26FdBdBvEhq4i8UsmDsQqH3iMY1AKmojZToZb5rStOZWHO/BZZ7nT2bscNjwm0E8 +6E2l6czk8ss= +-----END CERTIFICATE-----` + + x509CACertificateECDSA = ` +-----BEGIN CERTIFICATE----- +MIIBdzCCAR2gAwIBAgIQUzb62irYb/7B2H0c1AbriDAKBggqhkjOPQQDAjATMREw +DwYDVQQKEwhBdXRoZWxpYTAeFw0yMjA5MDgxMDEzNDZaFw0yMzA5MDgxMDEzNDZa +MBMxETAPBgNVBAoTCEF1dGhlbGlhMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +b/EiIBpmifCI34JdI7luetygue2rTtoNH0QXhtrjMuZNugT29LUz+DobZQxvGsOY +4TXzAQXq4gnTb7enNWFgsaNTMFEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdJQQIMAYG +BFUdJQAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUxlDPBKHKawuvhtQTN874 +TeCEKjkwCgYIKoZIzj0EAwIDSAAwRQIgAQeV01FZ/VkSERwaRKTeXAXxmKyc/05O +uDv6M2spMi0CIQC8uOSMcv11vp1ylsGg38N6XYA+GQa1BHRd79+91hC+7w== +-----END CERTIFICATE-----` +) + +func mustParseRSAPrivateKey(data string) *rsa.PrivateKey { + block, _ := pem.Decode([]byte(data)) + if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { + panic("not pem encoded") + } + + if block.Type != "RSA PRIVATE KEY" { + panic("not private key") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + panic(err) + } + + return key +} + +func mustParseX509Certificate(data string) *x509.Certificate { + block, _ := pem.Decode([]byte(data)) + if block == nil || len(block.Bytes) == 0 { + panic("not a PEM") + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + panic(err) + } + + return cert +} + +func buildChain(pems ...string) string { + buf := bytes.Buffer{} + + for i, data := range pems { + if i != 0 { + buf.WriteString("\n") + } + + buf.WriteString(data) + } + + return buf.String() +} + +func mustParseX509CertificateChain(datas ...string) *schema.X509CertificateChain { + chain, err := schema.NewX509CertificateChain(buildChain(datas...)) + if err != nil { + panic(err) + } + + return chain +} + func testInt32Ptr(i int32) *int32 { return &i } diff --git a/internal/configuration/provider.go b/internal/configuration/provider.go index 111e5c041..96db9c86b 100644 --- a/internal/configuration/provider.go +++ b/internal/configuration/provider.go @@ -61,6 +61,9 @@ func unmarshal(ko *koanf.Koanf, val *schema.StructValidator, path string, o inte StringToURLHookFunc(), StringToRegexpHookFunc(), StringToAddressHookFunc(), + StringToX509CertificateHookFunc(), + StringToX509CertificateChainHookFunc(), + StringToRSAPrivateKeyHookFunc(), ToTimeDurationHookFunc(), ), Metadata: nil, diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go index 3f7aa7b45..1f4b6db95 100644 --- a/internal/configuration/provider_test.go +++ b/internal/configuration/provider_test.go @@ -48,7 +48,7 @@ func TestShouldErrorSecretNotExist(t *testing.T) { errFmt := utils.GetExpectedErrTxt("filenotfound") - // ignore the errors before this as they are checked by the valdator. + // ignore the errors before this as they are checked by the validator. assert.EqualError(t, errs[0], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "authentication"), "authentication_backend.ldap.password", fmt.Sprintf(errFmt, filepath.Join(dir, "authentication")))) assert.EqualError(t, errs[1], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "duo"), "duo_api.secret_key", fmt.Sprintf(errFmt, filepath.Join(dir, "duo")))) assert.EqualError(t, errs[2], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "jwt"), "jwt_secret", fmt.Sprintf(errFmt, filepath.Join(dir, "jwt")))) diff --git a/internal/configuration/schema/const.go b/internal/configuration/schema/const.go index 8ee9867b2..c43eefb9d 100644 --- a/internal/configuration/schema/const.go +++ b/internal/configuration/schema/const.go @@ -57,3 +57,8 @@ const ( // regexpHasScheme checks if a string has a scheme. Valid characters for schemes include alphanumeric, hyphen, // period, and plus characters. var regexpHasScheme = regexp.MustCompile(`^[-+.a-zA-Z\d]+://`) + +const ( + blockCERTIFICATE = "CERTIFICATE" + blockRSAPRIVATEKEY = "RSA PRIVATE KEY" +) diff --git a/internal/configuration/schema/identity_providers.go b/internal/configuration/schema/identity_providers.go index 0bfb969d9..6c5f461f2 100644 --- a/internal/configuration/schema/identity_providers.go +++ b/internal/configuration/schema/identity_providers.go @@ -1,6 +1,7 @@ package schema import ( + "crypto/rsa" "net/url" "time" ) @@ -12,8 +13,9 @@ type IdentityProvidersConfiguration struct { // OpenIDConnectConfiguration configuration for OpenID Connect. type OpenIDConnectConfiguration struct { - HMACSecret string `koanf:"hmac_secret"` - IssuerPrivateKey string `koanf:"issuer_private_key"` + HMACSecret string `koanf:"hmac_secret"` + IssuerCertificateChain X509CertificateChain `koanf:"issuer_certificate_chain"` + IssuerPrivateKey *rsa.PrivateKey `koanf:"issuer_private_key"` AccessTokenLifespan time.Duration `koanf:"access_token_lifespan"` AuthorizeCodeLifespan time.Duration `koanf:"authorize_code_lifespan"` diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index e7bda004f..89622968a 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -18,6 +18,7 @@ var Keys = []string{ "log.file_path", "log.keep_stdout", "identity_providers.oidc.hmac_secret", + "identity_providers.oidc.issuer_certificate_chain", "identity_providers.oidc.issuer_private_key", "identity_providers.oidc.access_token_lifespan", "identity_providers.oidc.authorize_code_lifespan", diff --git a/internal/configuration/schema/types.go b/internal/configuration/schema/types.go index 5fb6de70e..6ac00a7a5 100644 --- a/internal/configuration/schema/types.go +++ b/internal/configuration/schema/types.go @@ -1,11 +1,18 @@ package schema import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "fmt" "net" "net/url" "strconv" "strings" + "time" ) // NewAddressFromString returns an *Address and error depending on the ability to parse the string as an Address. @@ -99,3 +106,161 @@ func (a Address) HostPort() string { func (a Address) Listener() (net.Listener, error) { return net.Listen(a.Scheme, a.HostPort()) } + +// NewX509CertificateChain creates a new *X509CertificateChain from a given string, parsing each PEM block one by one. +func NewX509CertificateChain(in string) (chain *X509CertificateChain, err error) { + if in == "" { + return nil, nil + } + + chain = &X509CertificateChain{ + certs: []*x509.Certificate{}, + } + + data := []byte(in) + + var ( + block *pem.Block + cert *x509.Certificate + ) + + for { + block, data = pem.Decode(data) + + if block == nil || len(block.Bytes) == 0 { + return nil, fmt.Errorf("invalid PEM block") + } + + if block.Type != blockCERTIFICATE { + return nil, fmt.Errorf("the PEM data chain contains a %s but only certificates are expected", block.Type) + } + + if cert, err = x509.ParseCertificate(block.Bytes); err != nil { + return nil, fmt.Errorf("the PEM data chain contains an invalid certificate: %w", err) + } + + chain.certs = append(chain.certs, cert) + + if len(data) == 0 { + break + } + } + + return chain, nil +} + +// X509CertificateChain is a helper struct that holds a list of *x509.Certificate's. +type X509CertificateChain struct { + certs []*x509.Certificate +} + +// Thumbprint returns the Thumbprint for the first certificate. +func (c *X509CertificateChain) Thumbprint(hash crypto.Hash) []byte { + if len(c.certs) == 0 { + return nil + } + + h := hash.New() + + h.Write(c.certs[0].Raw) + + return h.Sum(nil) +} + +// HasCertificates returns true if the chain has any certificates. +func (c *X509CertificateChain) HasCertificates() (has bool) { + return len(c.certs) != 0 +} + +// Equal checks if the provided *x509.Certificate is equal to the first *x509.Certificate in the chain. +func (c *X509CertificateChain) Equal(other *x509.Certificate) (equal bool) { + if len(c.certs) == 0 { + return false + } + + return c.certs[0].Equal(other) +} + +// EqualKey checks if the provided key (public or private) has a public key equal to the first public key in this chain. +// +//nolint:gocyclo // This is an adequately clear function even with the complexity. +func (c *X509CertificateChain) EqualKey(other any) (equal bool) { + if len(c.certs) == 0 || other == nil { + return false + } + + switch key := other.(type) { + case *rsa.PublicKey: + return key.Equal(c.certs[0].PublicKey) + case rsa.PublicKey: + return key.Equal(c.certs[0].PublicKey) + case *rsa.PrivateKey: + return key.PublicKey.Equal(c.certs[0].PublicKey) + case rsa.PrivateKey: + return key.PublicKey.Equal(c.certs[0].PublicKey) + case *ecdsa.PublicKey: + return key.Equal(c.certs[0].PublicKey) + case ecdsa.PublicKey: + return key.Equal(c.certs[0].PublicKey) + case *ecdsa.PrivateKey: + return key.PublicKey.Equal(c.certs[0].PublicKey) + case ecdsa.PrivateKey: + return key.PublicKey.Equal(c.certs[0].PublicKey) + case *ed25519.PublicKey: + return key.Equal(c.certs[0].PublicKey) + case ed25519.PublicKey: + return key.Equal(c.certs[0].PublicKey) + case *ed25519.PrivateKey: + switch pub := key.Public().(type) { + case *ed25519.PublicKey: + return pub.Equal(c.certs[0].PublicKey) + case ed25519.PublicKey: + return pub.Equal(c.certs[0].PublicKey) + default: + return false + } + case ed25519.PrivateKey: + switch pub := key.Public().(type) { + case *ed25519.PublicKey: + return pub.Equal(c.certs[0].PublicKey) + case ed25519.PublicKey: + return pub.Equal(c.certs[0].PublicKey) + default: + return false + } + default: + return false + } +} + +// Certificates for this X509CertificateChain. +func (c *X509CertificateChain) Certificates() []*x509.Certificate { + return c.certs +} + +// Validate the X509CertificateChain ensuring the certificates were provided in the correct order +// (with nth being signed by the nth+1), and that all of the certificates are valid based on the current time. +func (c *X509CertificateChain) Validate() (err error) { + n := len(c.certs) + now := time.Now() + + for i, cert := range c.certs { + if !cert.NotBefore.IsZero() && cert.NotBefore.After(now) { + return fmt.Errorf("certificate #%d in chain is invalid before %d but the time is %d", i+1, cert.NotBefore.Unix(), now.Unix()) + } + + if cert.NotAfter.Before(now) { + return fmt.Errorf("certificate #%d in chain is invalid after %d but the time is %d", i+1, cert.NotAfter.Unix(), now.Unix()) + } + + if i+1 >= n { + break + } + + if err = cert.CheckSignatureFrom(c.certs[i+1]); err != nil { + return fmt.Errorf("certificate #%d in chain is not signed properly by certificate #%d in chain: %w", i+1, i+2, err) + } + } + + return nil +} diff --git a/internal/configuration/schema/types_test.go b/internal/configuration/schema/types_test.go index 9d5da92c5..ab83941a1 100644 --- a/internal/configuration/schema/types_test.go +++ b/internal/configuration/schema/types_test.go @@ -1,10 +1,17 @@ package schema import ( + "crypto" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" "net" + "regexp" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewAddressFromString(t *testing.T) { @@ -48,3 +55,499 @@ func TestNewAddressFromString(t *testing.T) { }) } } + +func TestNewX509CertificateChain(t *testing.T) { + testCases := []struct { + name string + have string + thumbprintSHA256 string + err string + }{ + {"ShouldParseCertificate", x509CertificateRSA, + "956e53c127b18143241fb75d2149369d41ad1e4094c40cedcef22e468b37886b", ""}, + {"ShouldParseCertificateChain", x509CertificateRSA + "\n" + x509CACertificateRSA, + "956e53c127b18143241fb75d2149369d41ad1e4094c40cedcef22e468b37886b", ""}, + {"ShouldNotParseInvalidCertificate", x509CertificateRSAInvalid, "", + "the PEM data chain contains an invalid certificate: x509: malformed certificate"}, + {"ShouldNotParseInvalidCertificateBlock", x509CertificateRSAInvalidBlock, "", "invalid PEM block"}, + {"ShouldNotParsePrivateKey", x509PrivateKeyRSA, "", + "the PEM data chain contains a RSA PRIVATE KEY but only certificates are expected"}, + {"ShouldNotParseEmptyPEMBlock", x509CertificateEmpty, "", "invalid PEM block"}, + {"ShouldNotParseEmptyData", "", "", ""}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := NewX509CertificateChain(tc.have) + + switch len(tc.err) { + case 0: + switch len(tc.have) { + case 0: + assert.Nil(t, actual) + default: + assert.NotNil(t, actual) + + assert.Equal(t, tc.thumbprintSHA256, fmt.Sprintf("%x", actual.Thumbprint(crypto.SHA256))) + assert.True(t, actual.HasCertificates()) + } + assert.NoError(t, err) + default: + assert.Nil(t, actual) + assert.EqualError(t, err, tc.err) + } + }) + } +} + +func TestX509CertificateChain(t *testing.T) { + chain := &X509CertificateChain{} + + assert.Nil(t, chain.Thumbprint(crypto.SHA256)) + assert.False(t, chain.HasCertificates()) + assert.Len(t, chain.Certificates(), 0) + + assert.False(t, chain.Equal(nil)) + assert.False(t, chain.Equal(&x509.Certificate{})) + + assert.False(t, chain.EqualKey(nil)) + assert.False(t, chain.EqualKey(&rsa.PrivateKey{})) + + cert := MustParseCertificate(x509CertificateRSA) + cacert := MustParseCertificate(x509CACertificateRSA) + + chain = MustParseX509CertificateChain(x509CertificateRSA + "\n" + x509CACertificateRSA) + key := MustParseRSAPrivateKey(x509PrivateKeyRSA) + + thumbprint := chain.Thumbprint(crypto.SHA256) + assert.NotNil(t, thumbprint) + assert.Equal(t, "956e53c127b18143241fb75d2149369d41ad1e4094c40cedcef22e468b37886b", fmt.Sprintf("%x", thumbprint)) + + assert.True(t, chain.Equal(cert)) + assert.False(t, chain.Equal(cacert)) + assert.True(t, chain.EqualKey(key)) + + assert.NoError(t, chain.Validate()) + + chain = MustParseX509CertificateChain(x509CertificateRSA + "\n" + x509CertificateRSA) + assert.EqualError(t, chain.Validate(), "certificate #1 in chain is not signed properly by certificate #2 in chain: x509: invalid signature: parent certificate cannot sign this kind of certificate") + + chain = MustParseX509CertificateChain(x509CertificateRSAExpired + "\n" + x509CACertificateRSAExpired) + + err := chain.Validate() + require.NotNil(t, err) + assert.Regexp(t, regexp.MustCompile(`^certificate #1 in chain is invalid after 31536000 but the time is \d+$`), err.Error()) + + chain = MustParseX509CertificateChain(x509CertificateRSANotBefore + "\n" + x509CACertificateRSAotBefore) + + err = chain.Validate() + require.NotNil(t, err) + assert.Regexp(t, regexp.MustCompile(`^certificate #1 in chain is invalid before 13569465600 but the time is \d+$`), err.Error()) +} + +func MustParseX509CertificateChain(data string) *X509CertificateChain { + chain, err := NewX509CertificateChain(data) + if err != nil { + panic(err) + } + + if chain == nil { + panic("nil chain") + } + + return chain +} + +func MustParseCertificate(data string) *x509.Certificate { + block, x := pem.Decode([]byte(data)) + if block == nil { + panic("not pem") + } + + if len(x) != 0 { + panic("extra data") + } + + if block.Type != blockCERTIFICATE { + panic(fmt.Sprintf("not certifiate block: %s", block.Type)) + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + panic(err) + } + + return cert +} + +func MustParseRSAPrivateKey(data string) *rsa.PrivateKey { + block, x := pem.Decode([]byte(data)) + if block == nil { + panic("not pem") + } + + if len(x) != 0 { + panic("extra data") + } + + if block.Type != blockRSAPRIVATEKEY { + panic(fmt.Sprintf("not rsa private key block: %s", block.Type)) + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + panic(err) + } + + return key +} + +var ( + // Valid from 2022 to 2122 (years). + x509CertificateRSA = `-----BEGIN CERTIFICATE----- +MIIC6DCCAdCgAwIBAgIRAIxvm0gFgsbh3D22rSZLuFQwDQYJKoZIhvcNAQELBQAw +EzERMA8GA1UEChMIQXV0aGVsaWEwIBcNMjIxMDAyMDAzMDQyWhgPMjEyMjA5MDgw +MDMwNDJaMBMxETAPBgNVBAoTCEF1dGhlbGlhMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAy71EOkV3jOpVQtVTH5HYcI4PryUCiAEyxAIuO+66gaAa4aCd +UCRr8iO/pt5nOwPxjPo+hMHhkcKpX7evj+wgYXAccpIQFSCYWTJkaXFL0jL7yFuE +5xpjgRM/x6FfK0IbN5WmVWO9EjesbyMCyDoYpjwzIrxnB70F9Y0nrXst1SnW/Sy0 +01BQZNzD1tky1KDvEkw7L5mMPZFZMr5wV+ELvbo1LLvvrGYhhzbXWk7pPbxT0gAa +7yVvQbDKuCDqssAUyQa2JdlDaQocpldtK6l+dc3IsSWKd2UMouta75ngr9E1igy3 +t7owMRqH8NjwKHt6KQeDVSdBnWNjG572vaRimQIDAQABozUwMzAOBgNVHQ8BAf8E +BAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADANBgkqhkiG +9w0BAQsFAAOCAQEAaZJ09yGa+DGr/iTqshGdPKNCtcf/CXCkL52xiI7DzLxDt30P +8vCuXXrrTGBY7eWYupcNy/MyqaUrz1ED+map3nQzZQBJ9vWIfr01B9phkg/WSaNJ +1DlYtbPYzr86BlGP1V5d3Wv6JqF3tkWHI0kI38CT68fWdDKrfa5j3JdZGIVJW+51 +U0IE3Nqhfc76YzwQ3sNX5FT2Fr55RowH+l5OBPk0Bcztq58XmyPR/bvPfDASt8iS +DBT+0iiDiwk6LvOkasL8p7nuh5Grc9LMEYXY/QMUbkIWhIVRFlqyJA9s8vGHx1D4 +96iYKudj+yvO17Szzr/NNmcwETbCs4j6P6QeiA== +-----END CERTIFICATE-----` + + // Private Key for x509CertificateRSA. + x509PrivateKeyRSA = `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAy71EOkV3jOpVQtVTH5HYcI4PryUCiAEyxAIuO+66gaAa4aCd +UCRr8iO/pt5nOwPxjPo+hMHhkcKpX7evj+wgYXAccpIQFSCYWTJkaXFL0jL7yFuE +5xpjgRM/x6FfK0IbN5WmVWO9EjesbyMCyDoYpjwzIrxnB70F9Y0nrXst1SnW/Sy0 +01BQZNzD1tky1KDvEkw7L5mMPZFZMr5wV+ELvbo1LLvvrGYhhzbXWk7pPbxT0gAa +7yVvQbDKuCDqssAUyQa2JdlDaQocpldtK6l+dc3IsSWKd2UMouta75ngr9E1igy3 +t7owMRqH8NjwKHt6KQeDVSdBnWNjG572vaRimQIDAQABAoIBAA/EhhM8bRQqzo5t +lBFNaELNu8kCRD/iV9tzj8BzqVt+2JW9qG8bYn9K5Po1HCglFfyjIVOE7cAqIJGX +1a59x8PCuXDkfPolm6TLkZnXeta5u2K2MoLwN+M1aio5AvSGGTUkD8tr/KX8SQwQ +2ZZFaML0xcBadF7U8jEey4NRlSp5/voiIAB+FrJHepZBz2XJYCX5s2vYLPMn+51R +1HyO0n2aQ9H1Na8aBjTfAp9GDKJWBV3bSM7cVaLGlMFj/HNXUNVnSsVsJj0tdWKz +K6r9zPskLnS+eNjCgqrOtZSqJ7M3PL0/PoTFPrr1Fevr+soKWCaPF94Ib94O9SEq +scvP3kECgYEA0HBdGab0HjcZgFtsIaMm+eBcDhUmUrvMPUw6FmspKnc8wplscONW +wrDGhR0dpT8+aAMD5jFC2pvyHjI5AWkW+53LB15j6SVzUlUMfS3VTwE2prLtDHDs +nCDW2+fXY2kjv45efZGpMGbLJVePx2RCPzUlAlc14lzxnHgpo7eho1cCgYEA+jpi +Eo/Jqa5CNd4hrXqFxZTFtU2Mn38ZKI3QK/l47/347yHLebjsYIIwJRoHenxWxNlz +Y+BZ38vkP+f9BGAVGiRcyMmIJU0X305wKwl26Y2Q/tEm2OpwmDboD2pL9byi9vfY +bz7pQGK/l9j86KofRwVJJRLsofPI1SsjnC8c448CgYAkpg0IjJ1RjriSJADwLSKW +PseQxlE1rMVtZbC07mSPjeWGBbnWY3KGytQs5YCn5GXRne4alEC/9Tlt68CwKc0b +spPXGNaSUL5lFIUcoWlm+bylNMKPNG+1x+RfR/VMCll5vcuJYooP85L2Xt3t3gfz +2yFFtxXHVjY5H7uaiJgIAwKBgQDvkGXEj5TqtsL8/6YOiHb6Kuz+Hzi6mtxjTyI2 +d6mpWuWxTBGaf8kOvJWLb9gpFFGeNPGcdXaWJIZqCJjcT4Dkflu2f/uwepaYXGhX +S8Bk6fwfee5PTmRt1mNmHsaKhgcfmznDh9+YnPIBVuULe5RmUlEtBWk3xEZKj/qP +1Ss7UQKBgAwZQz+h5Z/XOJH3Qs5nJBKAZUiYkj3ux7G6tjx0cz7XcUYd/6enBpkY +JeqVHB6G+bMRLwb+Hc5Vgpbd5GdaUWo8udaghHgSGPUVcn0lK38XhYek6ACGz7Lo +xEfgtKoBlUq+uPb8H05HY0t9KybA3LA5wkRYYnJ17/nkZtrrJAmX +-----END RSA PRIVATE KEY-----` + + // Valid from 2022 to 2122 (years). + x509CACertificateRSA = `-----BEGIN CERTIFICATE----- +MIIDBTCCAe2gAwIBAgIQAK/NIAl3Bdg4Xk0y/ZGL7jANBgkqhkiG9w0BAQsFADAT +MREwDwYDVQQKEwhBdXRoZWxpYTAgFw0yMjEwMDIwMDMwMjFaGA8yMTIyMDkwODAw +MzAyMVowEzERMA8GA1UEChMIQXV0aGVsaWEwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCg7jdO1HmydfkPzTtz57pvAS3YOdBT0hlNjJ4N2lrKhNnixrzK ++4R1dWQDP2SHbZQ0TskF8eQ8HhTr7AsApotTthJFkUgV2g+bv7wVroz0Hok5xtd4 +bnpOvG3YUCP13Nk3ZVxdQXqR3/G3MrbyiXVPcgU+0giJ8EBykbtMu8L79/1iyk+m +w4fZfzTOeorRgspO3z3+pTAib2MCTA7bby1dX9qI/ysFPLdbJYfNQDxij8SzNLyJ +EkQ4kh3jKXf1VcZjbQTtYTZ3JJDqM08OxGMKuXUxPHd72Xlb+Fzql8LjYdEy/YKA +3r8FMf14lzcjvxtLnFXh//hiXh4+xgXMkrLZAgMBAAGjUzBRMA4GA1UdDwEB/wQE +AwICpDAPBgNVHSUECDAGBgRVHSUAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FGKpXiZA+8VQyMBqTTep+dVTthSbMA0GCSqGSIb3DQEBCwUAA4IBAQAE4DJg+Rb4 +iiocvxxQ85lhh94ql++E8MKuzIdN7ORs+ybUnsDD1WFDebubroTQuTSBkFrNuGNJ +8B7NZsHiWWLvNsrnxxeC5CicqfhSDti0rKWsbGyeoq7Kqok5E4pwOzeRsxL2e/Hm +G6LsUQuQMUG2vxKNynqmJS4VpgSVkiGhUfURFuRRDuRpVQ/XTl7jDIGf/ls7TAZq +1AnmnSi4Cqy4hrTnwYUYkFCcH69onUKAoaVNl1eAH7ogxakz32WyWObY98NBrjzA +I6VQlaQNSHtdFqDpu7NWJZZZSgN4BknbMYQEPNYCm701cPB4ahJbpg5C3pVPFSql +Bc9iI6nN3PCr +-----END CERTIFICATE-----` + + /* + // Private Key for x509CACertificateRSA. + x509CAPrivateKeyRSA = `-----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEAoO43TtR5snX5D807c+e6bwEt2DnQU9IZTYyeDdpayoTZ4sa8 + yvuEdXVkAz9kh22UNE7JBfHkPB4U6+wLAKaLU7YSRZFIFdoPm7+8Fa6M9B6JOcbX + eG56Trxt2FAj9dzZN2VcXUF6kd/xtzK28ol1T3IFPtIIifBAcpG7TLvC+/f9YspP + psOH2X80znqK0YLKTt89/qUwIm9jAkwO228tXV/aiP8rBTy3WyWHzUA8Yo/EszS8 + iRJEOJId4yl39VXGY20E7WE2dySQ6jNPDsRjCrl1MTx3e9l5W/hc6pfC42HRMv2C + gN6/BTH9eJc3I78bS5xV4f/4Yl4ePsYFzJKy2QIDAQABAoIBADlsZw3Y4Ufdsq6B + w/oasLqVSB+EmaKfMGosh+VXidgDyZ+S3KDtWJl09uf1wdBVOHHlvvNBGfidn0eD + pXVo+AQ5zpFGQtuRQMqJgvqVmzQshTi5i/8sJLZdpDBwgDRlxphusaORDsRojV6a + WQ94HwTnIZoF5ggaU1TOTXAW+39er+3CAkHZlqeCHliSdVWdAPy2AGTOKqTP3Pko + owbHkuCw53oWsDB9N8zqdVF+UBht0sFOQ8tEHq0OY1HJRtPhfvcFno9rADsna/Tg + 5m0sWUwP+uQ2+n18ahqunclzANu/w1SE+DVvXmeKdWMXEyuyL7muKsLgW5Ksa4jR + h6gAwBUCgYEAwmigbqAWrVhh5W1t388WcfsD0M7a4vMg7a3L5Im5mThgUHu3K5bM + EYLR8RnReEUdxdF86ASup4ddyVlz7z+YuQ9+XdTwMLK5Zujqfu9U/vsLHBc8h9eP + B7C6etfnTBfQeQryPaako8mixBpS0/pQhDhBHpWcYLLTh+uE17+C2U8CgYEA0+pe + EzWj7RHZiaHp6WWOqGuaCa3JR0uh2zns1gVx0cCdaCYFI+fOmrihrvBCYTXCiDZ4 + k4HGD48i1R8nUsV6HJfw0yhR2UmB46TnFYb0RgzuJhXlXtlivYVbSDhyzl1NBafC + 9zuCITMqGw921w/unOamMD94VSBfxT4EMBpvV1cCgYEAlhNuxfePigHQkOwJBd03 + 1oWQTIFjOA+4O8MOwz4OqNl8gKUAogWnQ11Z9GWZ7t5sPWmaowH6UhmNrQIBHZBa + tYHga08WnIFb3rWvUI4xbyUdTnIhqDwfjjA/xNUnGPbJWKe6mR0ru8TMgdZQWpPB + 1FAY9SNJtNxXr3WA94w/1sECgYBu0cEgioyPDSaVsvZ/93wC10JWjWsUvZiG7GPO + CErdRb0LGdbWUALbJnJm6X3NGDACy3mCqfrJaDDvArutrVeOXGa0BgHHf4lNYo71 + 0v0rJNflUs4AK+5W7cYunlZrVJ9StchfQd9rPTZnsE6VaN9/bZ663HYxDh0HKMdH + 4IsZQQKBgQCsBJ2bP9dc7QcqMXs8k37USIMs4nz6YWkV0Q74bBhbbnKIhY4mxhwf + 5WMBUntxYQT0yyygA8q/wJrjiWI9dr+Le0VAtg4Wn8CW0bNYvnMySn2++2E8m6jM + DBXePomOtkIwEdLGcO8csBLYQKn4x/5ONpYI2QAifIB8Gdxqnp5clg== + -----END RSA PRIVATE KEY-----` + */ + + // Valid from 1970 to 1971 (years). + x509CertificateRSAExpired = `-----BEGIN CERTIFICATE----- +MIIC5jCCAc6gAwIBAgIRAPKEPEnRO1hurtNAdEuDJA8wDQYJKoZIhvcNAQELBQAw +EzERMA8GA1UEChMIQXV0aGVsaWEwHhcNNzAwMTAxMDAwMDAwWhcNNzEwMTAxMDAw +MDAwWjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAN+qPAlnoqHMeBeXUF7qnaZXvHS4p8m2N9+hU8us6GYl3mYdFRDy +PGBYWewtIE0RsexBa7UYOV6IXdfheipsmRZZzUxjPbP/VfNuafxdZMVgQzWZZAtt +JJHRhLBATSfkutoPe3eUXxFonEhvl5ErU4327M9cZlLPRsIiVoTWOmigTT0jctx+ +u/3IyEVtV982SpttYnpCZ9lCvaSgjpvf1Mim+dbGF0KPKitAbuFnNpWsbRzIYfiy +rGMvxuftkywJ/e6Lx34HJjq/4+K1qII9clIiwAxa1RTnLbBuSLzVHxmj3L5hQhap +jf7HMhLReW2XLJNw4xUShSKpvapBRGbly18CAwEAAaM1MDMwDgYDVR0PAQH/BAQD +AgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcN +AQELBQADggEBAE5cRfJFTGOozfa9vOedNjif6MLqCtnVv9SRy7k/pdz0KSpYfpRo +dPJ0kBIV2JwIVAXfge6SNn5DK/fyDzERq/kQaCzgBZ6//hCf6Y1BApGajLvOmhPx +KijvMtqQ8IZnqwrURN04pQCt936grsas3Y6haxNyfdJDnQ5FgTe6kDOJU+rXyLp1 +N7H6hMXAd9+T++F50PM8AwtRZM6jSUVEhyrniKQSdkOQnXO5ng9if/7GntNzn56o +7cV3sBeenxEmvaXsR30C+A+Ankxr8HBlVOCYcJpbtsCmOB2PVRq9Q5KJAylHRWE1 +JedOdWjWvrVaP2IqRopS9mV3Ckf1E19YWFg= +-----END CERTIFICATE-----` + + /* + // Private Key for x509CertificateRSAExpired. + x509PrivateKeyRSAExpired = `-----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEA36o8CWeiocx4F5dQXuqdple8dLinybY336FTy6zoZiXeZh0V + EPI8YFhZ7C0gTRGx7EFrtRg5Xohd1+F6KmyZFlnNTGM9s/9V825p/F1kxWBDNZlk + C20kkdGEsEBNJ+S62g97d5RfEWicSG+XkStTjfbsz1xmUs9GwiJWhNY6aKBNPSNy + 3H67/cjIRW1X3zZKm21iekJn2UK9pKCOm9/UyKb51sYXQo8qK0Bu4Wc2laxtHMhh + +LKsYy/G5+2TLAn97ovHfgcmOr/j4rWogj1yUiLADFrVFOctsG5IvNUfGaPcvmFC + FqmN/scyEtF5bZcsk3DjFRKFIqm9qkFEZuXLXwIDAQABAoIBADBgYrHqD3wNfKAl + o0WUW1riOSnJ0sjHN9iPzU8NbArEABF4Etlie3qfQXva2tSwkho2oDRANBBlUF7k + LwdEC+yQqd3uzSbEgHOxmwzxql0ikAbk0YXDKpi7h4aTsdyCFYQauyrHFbTvOnZU + ZKUKiPz4vomvQ5Z/rJ9KzAnZSDLeqbJfBXPPitlE8DAiYypGKDUmX0unMJh/x0Pw + mIP/DTd+nMl+QpoSR0nS8r8Pr+4oBJ8K6k9Oni2DKdIW8IvoQJBBa9cm8Y0fHkSl + hB7fncY5bE0lOZ8jBlSNuGfZHjVihwBA+rYAcWpyzdBx3SHRSe5AH4RKBPaERgSt + SBV35PECgYEA7ayQs2SMggOiEK4Wf9AzywieaHiHa2ObJRg2dHlIgVkUDL3zB/b6 + 57jPMXAtMyGQDW6pZF6Oq3mgYP2A9alB6QKjpX1OGFmqZJxtRMAm0KPs2C2inWzg + dz9OW18jDlKKsHR00JktqsNgOZC8ldE2cyqgwBNXT/P9GyUMC9RmYhkCgYEA8Okk + 9u8IoIHJEWbtmmh0d1CEmPz4zQosTgUl2GLbNaCE/zDvA82YUQmi1yaF1FHjXMoa + tD0Plkixoj/ezASeSE+duVpgXflYL4IHbqQq9JQg39vyaSuU1g3wP+hnmnCT62vb + z4v7ugDLLkSlvNeEQLR3GvZvInZnfdwI5/mjeDcCgYA1UGlhJGP0YjY/gZ2gbCbC + G5vVGXxfFYfeyVClzfL6uO2rcgyLM9bSlf08PMqW1qeGq9Upo6BjTLQyLYt5D8+u + Ih5tZ+9VvP9g9En6ixPp52ugjpQUtjCf7z53dp7ZfqCHtofhpwq8bHkwUIxNGxIY + wW4vx+blE3kqVqQeHzYcOQKBgBgBAv/fvVpQ1Dn5qX8THVeuHCgqPJghhVyYwraW + 0wS648WRmJ8mYyDf9uu9GOSY7DCYqqR+2Qi+YYSrHIXzh9nopOyNBsEWUSUarabm + kKkiAUyM29CC2Sei5+dWPsxynyp76sD5T7Gu1o/boy/3wWO5F40GNPiYF6PAwtpq + U1FtAoGBANfr5OcnCIdtHLCEVRCaJdzTkQj5X1g9dF0D/gWkBIF0hibcs9yV2i1Z + JtxBrOvctkRsY7/C8dCms1gkfwDyTpKuMk9iDd3wfDGP3LdD1+V10pCm5ShHIGNm + /pRFpN45nR5iCX9mnvr8YJLUsrBkh7N4c4ao8xsXzOLBOk8WvtXL + -----END RSA PRIVATE KEY-----` + */ + + // Valid from 1970 to 1971 (years). + x509CACertificateRSAExpired = `-----BEGIN CERTIFICATE----- +MIIDAzCCAeugAwIBAgIQB07G1WhPAiAHaM6FogkZ7TANBgkqhkiG9w0BAQsFADAT +MREwDwYDVQQKEwhBdXRoZWxpYTAeFw03MDAxMDEwMDAwMDBaFw03MTAxMDEwMDAw +MDBaMBMxETAPBgNVBAoTCEF1dGhlbGlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEArly1jZXqLaAQJwTaoT0QaePV7SejQZj418Id7X7Gx1bgjmIS0mL8 +058c8Av1ThRaUmVAMkbBc4MED9KnKKckU+qMBV0+2xovrn7TvAKHG3yLUtasn/Uz +2UzBXMgbCIMVCloWkPQ6BuSaNsZnSBmTj/IC16bAZwm5lIS9ZjzQ+QnZg8x5ftNR +Jx8Gar8xb4tQbhb6uZU5zxfuV1+4qk04lf6E48IVrf57NBXpJhdzxuvdBj0l46k3 +zf44gybrXpr9O0n0Eb/H8lkIoIDM+vanoRvdg868QQw8C/r06M4E8gJJzZk7Ad2c +oCq66peom6eLUSo2DVfVmmQ2KjLUyW0L6QIDAQABo1MwUTAOBgNVHQ8BAf8EBAMC +AqQwDwYDVR0lBAgwBgYEVR0lADAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSQ +qzXTP6jV517cAknynvP0vr4ChzANBgkqhkiG9w0BAQsFAAOCAQEAGvX14cJM+uxh +rSBYa3qHEfSvlSwbQsCxZv5VXLJj6SLYUXqMLxCuPH16ZKD8MygmnF5/8Mp3ZIdF +Aesehtaoth1H06q6iXWw+wgT9fZKGIHL4RftvjFWncD+Lbk8hP/DkLqsGt+Mj23u +JAByhiG8QmbXu/X7kfXSvXjhQ7f7C+bNKxb03r7mT5gI8mCUp5MyLp3DPk3dKTwa +uby/wjlFMHi92HjfQ6mCn5ijc/ltiMh1wtXf53IEESYvrWV5ABjV8xnumI4j4idB +7yHjCn5id379go8e8a+W8jODNzUORzSio8tDhL4c13tiD4PzlMJ1tUr+CIoCzqB5 +m99SvwJ74A== +-----END CERTIFICATE-----` + + /* + // Private Key for x509CACertificateRSAExpired. + x509CAPrivateKeyRSAExpired = `-----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEArly1jZXqLaAQJwTaoT0QaePV7SejQZj418Id7X7Gx1bgjmIS + 0mL8058c8Av1ThRaUmVAMkbBc4MED9KnKKckU+qMBV0+2xovrn7TvAKHG3yLUtas + n/Uz2UzBXMgbCIMVCloWkPQ6BuSaNsZnSBmTj/IC16bAZwm5lIS9ZjzQ+QnZg8x5 + ftNRJx8Gar8xb4tQbhb6uZU5zxfuV1+4qk04lf6E48IVrf57NBXpJhdzxuvdBj0l + 46k3zf44gybrXpr9O0n0Eb/H8lkIoIDM+vanoRvdg868QQw8C/r06M4E8gJJzZk7 + Ad2coCq66peom6eLUSo2DVfVmmQ2KjLUyW0L6QIDAQABAoIBAAiOZBpekO9MO36u + rkvbQ0Lu+0B4AXrmls9/pxhQcFC34q0aAvJwCRgZZsIg1BjQxt3kOhI9hqC0fS6J + l8pW6WF00QoyWTNHRa+6aYmAVkDzC6M1BaOT1MeFDLgQ2cLBK/cmFJVoZrCP50Fo + 2wieuK8HoTwT4r0rrP+sw96QfXC7BjC1VSL9GXYemKz0RXEUvXXmzGGc9YE8vCt/ + PXOb4TV30TIQrivkywSTJi8A1jUjYI2rPgo6JCl6GZGmc7hVX4jJ9lbBhUH76ozO + KS1Yzo/veWL4rVspc2exT5cuX7JIuFCjVi0Nlv1MKv39jpfTfKQh0ug6pHlxUzqX + Rl6Ln0ECgYEAwX0HtsmKMoSIrU4k90TKyhzjNoL8EaMKrSoZ4nK599igd8g+y6eD + jc1qO60lOHObyLPFor0rQT7K2KCD7GKYSX5+sh9leQEASl7cJkSAG17WBlrf9Pas + nUXjTRx3moEILAWmuov4UrYpkuEFk65d98xP3uPtDylFj57Bc+a8DOcCgYEA5rHK + qdjE8c0/1kgmItDJjmKxrJ5K5hY4w7SkpZeg4Rwr2WAjv3je/nx34D8S7m6R2uzp + NQYAAHXdzHt4iegupyW/3UXJboEscSTuC6/v3llawAozh02nDsMrdC1LreQ1IiFy + mKDmPZWxiAZXxEJ0hi0YMCcQnBY673eAleostq8CgYEAtia/mVvYh0Bv7z9O253e + jzFs0ce0B+KGzYiB/8XjvyknwDw6qbzUwy0romyZSrDDasma+F7AFtdHXXKXX3Ve + SmoUWhnmjGjd3iW5eSkptRqtwCPTDKkgzZqapuBy1Hg+ujrDwIC+0Rb+wnCmsGYJ + vpuQYZQPeyNugguBsVv5kucCgYBToTRE6k5LEgsIVVNt356RvXmHiELCsl+VotDl + Ltilgp7qyI1tBhZgzyJt6q+kO/UoFiZckHZDtHbZgBEsfT0cXvT09C2Xn8BKrAaX + ugoM4vuhDpGrhR0AnwQLs7fxq/8PBm0So5GT1cZr91Ct1yGC2qogGqlMzEpFMV8t + +ZyIBQKBgFyI2cZ3/uHQMkWUguml9w134bIGpqGdp8jf9YTvWs3Ax8/qxAVmFmtm + fot+QiamambblrhdT6pau11Mp06FzytorQH0qKd8mPAqvqtvcSoDZzqXjrkUnZIx + uLUcfb41clRhlfGDUj5MWimfi7d/Vakh86zAa8pg5WBCtXr0bZ/j + -----END RSA PRIVATE KEY-----` + */ + + // Valid from 2400 to 2401 (years). + x509CertificateRSANotBefore = `-----BEGIN CERTIFICATE----- +MIIC6TCCAdGgAwIBAgIQYQWHYM90zNnwv22xhOPkszANBgkqhkiG9w0BAQsFADAT +MREwDwYDVQQKEwhBdXRoZWxpYTAiGA8yNDAwMDEwMTAwMDAwMFoYDzI0MDAxMjMx +MDAwMDAwWjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAPFYYindnRfs8aJfXbaX5IRVj10uKlT4i0BwJ5IYaC4O/3UQ +km7do8lL2Ea2N2L5tQJhk2d+yoWGPeaUyuYP692jPA+4BW6RPuroSPB9WEU+x1ir +it/AzJtavg0Lu2fGZkXxAZJj2MlrXT7csaGwRAvvPEHS6EJW4UtERYIqfpKGB39I +sUhKNvY3edF9sosUAJmiZ8Q4K/uYoyCxyiE1QKLaiIjcZJxtzXkzwVBy1ZlmG+r5 +VNNguQQFsS8f7uRlOmo0o3hDG9dByUn7PgFEExbgBtdmNoIPk/pfMFM8NIHK+wOC +q/SO2e/MX0IhJZXfq2VTZFgrisPovg8GpHSHRCkCAwEAAaM1MDMwDgYDVR0PAQH/ +BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJKoZI +hvcNAQELBQADggEBADBTRbfg/UQeJpdMogm9tleXJBcHqgOgiBxkKYxGSlRg4vlr +tM8USAr24whLvb8KDhT6PaSY8wyPuCxqwqiKR84eselxOAcgDLV9n36OcWRm+oFl +Th1S1JUtjbrctU7i6pp4BwUmBwkVALrbrj+cGVG7uBfbP8L/onmjg9KkY5ttnbyb +Qi/Pv9zYLEo394hL3oeaphkP0iE6cHOvII/qFhnpLREINGp3g8V2I46Id/xYDi22 +WRabgMuFGpel7Q26yh+YXyHoIkKdiOXMNNXTsuvp5EDBGybTIXWK3xrqTsREMVDr +EmiaOgL8c7+PpSWuUggJLb/JXDYnPtvekH3gPao= +-----END CERTIFICATE-----` + + /* + // Private Key for x509CertificateRSANotBefore. + x509PrivateKeyRSANotBefore = `-----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEA8VhiKd2dF+zxol9dtpfkhFWPXS4qVPiLQHAnkhhoLg7/dRCS + bt2jyUvYRrY3Yvm1AmGTZ37KhYY95pTK5g/r3aM8D7gFbpE+6uhI8H1YRT7HWKuK + 38DMm1q+DQu7Z8ZmRfEBkmPYyWtdPtyxobBEC+88QdLoQlbhS0RFgip+koYHf0ix + SEo29jd50X2yixQAmaJnxDgr+5ijILHKITVAotqIiNxknG3NeTPBUHLVmWYb6vlU + 02C5BAWxLx/u5GU6ajSjeEMb10HJSfs+AUQTFuAG12Y2gg+T+l8wUzw0gcr7A4Kr + 9I7Z78xfQiElld+rZVNkWCuKw+i+DwakdIdEKQIDAQABAoIBACGcZHdeJK2bUv+A + 9oUiXDHN1JxufHi+8G218NzYx1F6xzrfZvVHqrKy/FjEsav4CKxfOG8Wak/0JRTC + rgsiNn/0Zr3tq9v9IF0IonfTjQJ/vrVrlniY2iXcmlEozB2ktMOSz9w6SYurhx3l + EFvrN17OH38vRydOACxCQsfg8SWofY6SV0gcvlCcuM4lKBiuOBWGcf+xwIs3B+Bs + Frd282jRWtlcYd+zDE+vLxugNizLGpRKCMEdcKPRw9fkBKDI/f56WegNTUZYYFrV + LEmYIbOwMawvbi0mOdLsp27CfmeUjkEbwzgdNwjFrWIFAk0wT3QvDrKxDYDLM2Z3 + +PtBMwECgYEA9ICYgzPMbN3CsQ+eWSQXXNk35V7PlMl1DC4UIHhi53BMT1ZhvkHf + D+eqXQ3BSqOUR7b417VBGkK8UtQuQXh9FwwVU0RhVkjpX0nTBhe8gGF3f3094rX4 + Ckhm8XYQEWCUA9HNhCW+KSNVWqgw9Qi0awEY7HaiR628br39/EckvokCgYEA/LHI + HA9ixEBeTjds52rK6n9bPHeI87qxF62lLQYXvosyJij9/ruUfXwfJjG3EvCfcW7N + fr2EvgzPbCozC1V6gI9k5CXhOsf+wD6M8A7g7YHUa/dPq2B8bfqaMD0vW7OoZiLQ + NpfMtBvZxd1wukPGypLGWabPLo8u6bSfxTqz8KECgYAudnmFBUTls0aaKyOmQOuH + o2ex2NCNr7Lke6UrfnUdEgQOV5X/d7kR5q5DPKfsrSUyc5zaMQGMIf5zpwqbOnBa + /trWlfoBUZ23k+ncEIqrwtnYik5GVNor6hJV9F+dTcMS7r2lTR7T5nkD305eYicW + 5oB7/xdbk7JpQQWQ+VwMMQKBgQC3hbKs1mvH1mvnaI+aftACgR5VCweW4/bsGHwG + +A7Unyl713eowrk0ban9xkuM4N8btfpe2uuGT61xhDBwQdNnfT0sCWrLkyasnoEj + c9reA9Wv1/yvnbKg+Ul0UWuMsS1TiGMp0xOjlzqRXqMZVFITG4gc4m5EBU9wAnOq + /VhkIQKBgBUwpoL+K1OswQKV/SHH7n5b/By02mLuMkQR0NlpWRJY6eTDk3FAUkHn + +T996U0a+7OY+mATVqrfLBcsa6i+HGpb4jZL+kkdtfmtHUnN8YOAwopWp3uoPRmF + lpakAQq8NPVcrX6PLDkHeOlKhE4ercYFqcCRKcnMB3nD+re6x5l6 + -----END RSA PRIVATE KEY-----` + */ + + // Valid from 2400 to 2401 (years). + x509CACertificateRSAotBefore = `-----BEGIN CERTIFICATE----- +MIIDBzCCAe+gAwIBAgIQeR2/TbyH9gEzyjuTijMGVzANBgkqhkiG9w0BAQsFADAT +MREwDwYDVQQKEwhBdXRoZWxpYTAiGA8yNDAwMDEwMTAwMDAwMFoYDzI0MDAxMjMx +MDAwMDAwWjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAJ7vpRSDXVvwOLGmjbZdoG25OdsWgmhVAWpFCUifotqorz+z ++VgwPnvVDp1cTp07y+mDJK++GNBOG1pS5G4or6Y1HAlT3nGpE0FYhrtDBQmhvqV0 +6mPM/Dq5JIuGiju4LX0KBlaFJugJevw3ySnoPUu0BQ9mTZUgggNwetqsAX7TioFj +TkVIMtgranigOvWjJQyLlmiK1TOgMqgWYNR20SE4CkmIp9SjOdeW7kNVMOojRx9a +VgElA2TN57/Je+/tLbvTDDCP9o59SIXxn5N6JQ2/XdDZPNBHrxnmchQVDXWCkP0A +gkV7V1dl8ur85iEdN+F31Kvd0nzCCaC3YUMxgmcCAwEAAaNTMFEwDgYDVR0PAQH/ +BAQDAgKkMA8GA1UdJQQIMAYGBFUdJQAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUB4/oEXJWhMwKBpMesSOnQdD+G0UwDQYJKoZIhvcNAQELBQADggEBAIlzOnWB +xIhMm3zpLfpJGBi62d9J7Rlf5NitWoztyHdJpQ9y99s67QonR7UY7n11PMdtsLja +hWy1/iZ1o5F2zGKPzSS8pscdIuo4B+TocLiHkEx7ttyQ0MepoDt1RlTOjqilqbfD +A4GyGidns1VZuH8wP8NpZNlWajsXgvkYT433RzPgKe7qoI3DFQwc72SBuZSHHyjE +9SVgdN0KmfFXMum4BurwftelF1etGR+4II3cDG80CH2ZvYdqCURPoa+ny/qqMtzq +W2CnwP59TrotQgKCFJS5EdL3MXaZSvK9z2LERdxDvp4OSoJYoxSMJawfkVwZ15rk +apA21VwIrpFg54A= +-----END CERTIFICATE-----` + + /* + // Private Key for x509CACertificateRSAotBefore. + x509CAPrivateKeyRSANotBefore = `-----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEAnu+lFINdW/A4saaNtl2gbbk52xaCaFUBakUJSJ+i2qivP7P5 + WDA+e9UOnVxOnTvL6YMkr74Y0E4bWlLkbiivpjUcCVPecakTQViGu0MFCaG+pXTq + Y8z8Orkki4aKO7gtfQoGVoUm6Al6/DfJKeg9S7QFD2ZNlSCCA3B62qwBftOKgWNO + RUgy2CtqeKA69aMlDIuWaIrVM6AyqBZg1HbRITgKSYin1KM515buQ1Uw6iNHH1pW + ASUDZM3nv8l77+0tu9MMMI/2jn1IhfGfk3olDb9d0Nk80EevGeZyFBUNdYKQ/QCC + RXtXV2Xy6vzmIR034XfUq93SfMIJoLdhQzGCZwIDAQABAoIBAEneATBOeYZwWDkg + un5Gd3hnfN85T/SjhVvZqB3rq6nKemC2Ca4WBgRRmlBChXsIPpZR0CwpwqiVlJrf + KbGVEUXDKzuekiTrOrrFJSFFXcMDPHLzqrglnhjA0Z5TMk3dJK8XiKiPi+yN823j + k4f5mvtjOHLWzjn/+M0WatLU3IEPnqpnE+pEKrkZQa7Mg+xHprvt67Q4aCgP5lfy + A04eoUo7+TMRsK718vb02E81ZQSLgSbQMd0W8Dkt7vRkYRNL0OKBlPQcP9qZlw5s + swy4ne9jgmJKY3mmdnURTjvdJb20dUsSSzseZ8Tj6UYUDntXrB62YhZvC0ZRhGY/ + Nnf10IECgYEAzSi1l1G6ZblV2g2jPqqD4EsdUvitnN5t592dw52+SyizNj7j+pLt + OPi+bt5HW4orHQHlPi6wt1BQIZ7UVHmljKQq7ByxOX0SRrRg428JPlFMjCp3bG1t + zmRQwADGkfqn3JQcmY9VDjtn5oCx18bNpDR1gNiK6zImK/jmWBOiaKcCgYEAxlKN + vYeG70ZrtVBz6jmX6yOtN8/hBA0mifZVHmkKX6earU3Ds2Uj2Hg+AgMqoFD2aRSp + wodEYzV6hSpvdLzqBi6SklnfF4NBqJ51TEFBWaVMUZjTwConOcc+vvvSDbCkmnoF + yTcqVm2p91HD7859ACcO+m8nsFJGldbJFl8RkEECgYEAkSLajEkyH2Kk3JTHRr7k + eplJDniEgbRNdjmusUN36r3JQnftWkf08FfwiIhRXO37IBNGNN5c/+IePhqZxYUl + W8CL6OtHaQ8VDdXvsRXNKTvkdkhYoeksRFVtVtd1orH7bK2PKgdfOalHEKc8qRSo + SCEge101sbuRi4wSkH6bZ4MCgYAerDXv0j40U5fk+wRyfWXZoDLyJtyOW9pSDB8u + DODl2m45z4UtAb+Bg1dTyFmXYe46Yk+/HlydW3APmHiUfYNUYW+Z4vx2Dn7hLWDG + 4nDRBJfBJvnZBqv6a65wq1HZfDB5E9ZBQJ7zrxJShfrf4/fBRkkywm5I/vCbzBRd + uWZmAQKBgQCJE5rx3rWZz+srunmmkg5LXBMveD+1HRvlwlj/gELG2b2ytNUmnVey + 2naApHnvW7lZdrADpbzLKGEDB/EsaIPJjqQw45OoIZwPdM4bm0/w5c1ZLnStXGCz + Th/7Sva6x6FW7tHY6ldqybcMj8w3kA4ByQEOg2BtPnWTm1NX/qcr8Q== + -----END RSA PRIVATE KEY-----` + */ + + x509CertificateRSAInvalid = `-----BEGIN CERTIFICATE----- +mIIC5jCCAc6gAwIBAgIRAK4Sj7FiN6PXo/urpfO4E7owDQYJKoZIhvcNAQELBQAw +EzERMA8GA1UEChMIQXV0aGVsaWEwHhcNNzAwMTAxMDAwMDAwWhcNNzEwMTAxMDAw +MDAwWjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAPKv3pSyP4ozGEiVLJ14dIwFCEGEgq7WUMI0SZZqQA2ID0L59U/Q +/Usyy7uC9gfMUzODTpANtkOjFQcQAsxlR1FOjVBrX5QgjSvXwbQn3DtwMA7XWSl6 +LuYx2rBYSlMSN5UZQm/RxMtXfLK2b51WgEEYDFi+nECSqKzR4R54eOPkBEWRfvuY +91AMjlhpivg8e4JWkq4LVQUKbmiFYwIdK8XQiN4blY9WwXwJFYs5sQ/UYMwBFi0H +kWOh7GEjfxgoUOPauIueZSMSlQp7zqAH39n0ZSYb6cS0Npj57QoWZSY3ak87ebcR +Nf4rCvZLby7LoN7qYCKxmCaDD3x2+NYpWH8CAwEAAaM1MDMwDgYDVR0PAQH/BAQD +AgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcN +AQELBQADggEBAHSITqIQSNzonFl3DzxHPEzr2hp6peo45buAAtu8FZHoA+U7Icfh +/ZXjPg7Xz+hgFwM/DTNGXkMWacQA/PaNWvZspgRJf2AXvNbMSs2UQODr7Tbv+Fb4 +lyblmMUNYFMCFVAMU0eIxXAFq2qcwv8UMcQFT0Z/35s6PVOakYnAGGQjTfp5Ljuq +wsdc/xWmM0cHWube6sdRRUD7SY20KU/kWzl8iFO0VbSSrDf1AlEhnLEkp1SPaxXg +OdBnl98MeoramNiJ7NT6Jnyb3zZ578fjaWfThiBpagItI8GZmG4s4Ovh2JbheN8i +ZsjNr9jqHTjhyLVbDRlmJzcqoj4JhbKs6/I= +-----END CERTIFICATE-----` + + x509CertificateRSAInvalidBlock = `-----BEGIN CERTIFICATE----- +MIIC5jCCAc6gAwIBAgIRAK4Sj7FiN6PXo/urPfO4E7owDQYJKoZIhvcNAQELBQAw +EzERMA8GA1UEChMIQXV0aGVsaWEwHhcNNzAwMTAxMDAwMDAwWhcNNzEwMTAxMDAw +MDAwWjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAPKv3pSyP4ozGEiVLJ14dIWFCEGEgq7WUMI0SZZqQA2ID0L59U/Q +/Usyy7uC9gfMUzODTpANtkOjFQcQAsxlR1FOjVBrX5QgjSvXwbQn3DtwMA7XWSl6 +LuYx2rBYSlMSN5UZQm/RxMtXf^K2b51WgEEYDFi+nECSqKzR4R54eOPkBEWRfvuY +91AMjlhpivg8e4JWkq4LVQUKbmiFYwIdK8XQiN4blY9WwXwJFYs5sQ/UYMwBFi0H +kWOh7GEjfxgoUOPauIueZSMSlQp7zqAH39N0ZSYb6cS0Npj57QoWZSY3ak87ebcR +Nf4rCvZLby7LoN7qYCKxmCaDD3x2+NYpWH8CAwEAAaM1MDMwDgYDVR0PAQH/BAQD +AgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcN +AQELBQADggEBAHSITqIQSNzonFl3DzxHPEzr2hp6peo45buAAtu8FZHoA+U7Icfh +/ZXjPg7Xz+hgFwM/DTNGXkMWacQA/PaNWvZspgRJf2AXvNbMSs2UQODr7Tbv+Fb4 +lyblmMUNYFMCFVAMU0eIxXAFq2qcwv8UMcQFT0Z/35s6PVOakYnAGGQjTfp5Ljuq +wsdc/xWmM0cHWube6sdRRUD7SY20KU/kWzl8iFO0VbSSrDf1AlEhnLEkp1SPaxXg +OdBnl98MeoramNiJ7NT6Jnyb3zZ578fjaWfThiBpagItI8GZmG4s4Ovh2JbheN8i +ZsjNr9jqHTjhyLVbDRlmJzcqoj4JhbKs6/I= +-----END CERTIFICATE-----` + + x509CertificateEmpty = `-----BEGIN CERTIFICATE----- +-----END CERTIFICATE-----` +) diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 78797a283..a41c65420 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -132,6 +132,8 @@ const ( errFmtOIDCNoClientsConfigured = "identity_providers: oidc: option 'clients' must have one or " + "more clients configured" errFmtOIDCNoPrivateKey = "identity_providers: oidc: option 'issuer_private_key' is required" + errFmtOIDCCertificateMismatch = "identity_providers: oidc: option 'issuer_private_key' does not appear to be the private key the certificate provided by option 'issuer_certificate_chain'" + errFmtOIDCCertificateChain = "identity_providers: oidc: option 'issuer_certificate_chain' produced an error during validation of the chain: %w" errFmtOIDCEnforcePKCEInvalidValue = "identity_providers: oidc: option 'enforce_pkce' must be 'never', " + "'public_clients_only' or 'always', but it is configured as '%s'" @@ -294,9 +296,11 @@ var validSessionSameSiteValues = []string{"none", "lax", "strict"} var validLoLevels = []string{"trace", "debug", "info", "warn", "error"} var validWebauthnConveyancePreferences = []string{string(protocol.PreferNoAttestation), string(protocol.PreferIndirectAttestation), string(protocol.PreferDirectAttestation)} + var validWebauthnUserVerificationRequirement = []string{string(protocol.VerificationDiscouraged), string(protocol.VerificationPreferred), string(protocol.VerificationRequired)} var validRFC7231HTTPMethodVerbs = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"} + var validRFC4918HTTPMethodVerbs = []string{"COPY", "LOCK", "MKCOL", "MOVE", "PROPFIND", "PROPPATCH", "UNLOCK"} var validACLHTTPMethodVerbs = append(validRFC7231HTTPMethodVerbs, validRFC4918HTTPMethodVerbs...) @@ -306,9 +310,13 @@ var validACLRulePolicies = []string{policyBypass, policyOneFactor, policyTwoFact var validDefault2FAMethods = []string{"totp", "webauthn", "mobile_push"} var validOIDCScopes = []string{oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopeGroups, oidc.ScopeOfflineAccess} + var validOIDCGrantTypes = []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"} + var validOIDCResponseModes = []string{"form_post", "query", "fragment"} + var validOIDCUserinfoAlgorithms = []string{"none", "RS256"} + var validOIDCCORSEndpoints = []string{oidc.AuthorizationEndpoint, oidc.TokenEndpoint, oidc.IntrospectionEndpoint, oidc.RevocationEndpoint, oidc.UserinfoEndpoint} var reKeyReplacer = regexp.MustCompile(`\[\d+]`) diff --git a/internal/configuration/validator/identity_providers.go b/internal/configuration/validator/identity_providers.go index 4cdfa3800..ae0f4808c 100644 --- a/internal/configuration/validator/identity_providers.go +++ b/internal/configuration/validator/identity_providers.go @@ -16,45 +16,60 @@ func ValidateIdentityProviders(config *schema.IdentityProvidersConfiguration, va } func validateOIDC(config *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) { - if config != nil { - if config.IssuerPrivateKey == "" { - validator.Push(fmt.Errorf(errFmtOIDCNoPrivateKey)) + if config == nil { + return + } + + setOIDCDefaults(config) + + if config.IssuerPrivateKey == nil { + validator.Push(fmt.Errorf(errFmtOIDCNoPrivateKey)) + } else if config.IssuerCertificateChain.HasCertificates() { + if !config.IssuerCertificateChain.EqualKey(config.IssuerPrivateKey) { + validator.Push(fmt.Errorf(errFmtOIDCCertificateMismatch)) } - if config.AccessTokenLifespan == time.Duration(0) { - config.AccessTokenLifespan = schema.DefaultOpenIDConnectConfiguration.AccessTokenLifespan + if err := config.IssuerCertificateChain.Validate(); err != nil { + validator.Push(fmt.Errorf(errFmtOIDCCertificateChain, err)) } + } - if config.AuthorizeCodeLifespan == time.Duration(0) { - config.AuthorizeCodeLifespan = schema.DefaultOpenIDConnectConfiguration.AuthorizeCodeLifespan - } + if config.MinimumParameterEntropy != 0 && config.MinimumParameterEntropy < 8 { + validator.PushWarning(fmt.Errorf(errFmtOIDCServerInsecureParameterEntropy, config.MinimumParameterEntropy)) + } - if config.IDTokenLifespan == time.Duration(0) { - config.IDTokenLifespan = schema.DefaultOpenIDConnectConfiguration.IDTokenLifespan - } + if config.EnforcePKCE != "never" && config.EnforcePKCE != "public_clients_only" && config.EnforcePKCE != "always" { + validator.Push(fmt.Errorf(errFmtOIDCEnforcePKCEInvalidValue, config.EnforcePKCE)) + } - if config.RefreshTokenLifespan == time.Duration(0) { - config.RefreshTokenLifespan = schema.DefaultOpenIDConnectConfiguration.RefreshTokenLifespan - } + validateOIDCOptionsCORS(config, validator) - if config.MinimumParameterEntropy != 0 && config.MinimumParameterEntropy < 8 { - validator.PushWarning(fmt.Errorf(errFmtOIDCServerInsecureParameterEntropy, config.MinimumParameterEntropy)) - } - - if config.EnforcePKCE == "" { - config.EnforcePKCE = schema.DefaultOpenIDConnectConfiguration.EnforcePKCE - } - - if config.EnforcePKCE != "never" && config.EnforcePKCE != "public_clients_only" && config.EnforcePKCE != "always" { - validator.Push(fmt.Errorf(errFmtOIDCEnforcePKCEInvalidValue, config.EnforcePKCE)) - } - - validateOIDCOptionsCORS(config, validator) + if len(config.Clients) == 0 { + validator.Push(fmt.Errorf(errFmtOIDCNoClientsConfigured)) + } else { validateOIDCClients(config, validator) + } +} - if len(config.Clients) == 0 { - validator.Push(fmt.Errorf(errFmtOIDCNoClientsConfigured)) - } +func setOIDCDefaults(config *schema.OpenIDConnectConfiguration) { + if config.AccessTokenLifespan == time.Duration(0) { + config.AccessTokenLifespan = schema.DefaultOpenIDConnectConfiguration.AccessTokenLifespan + } + + if config.AuthorizeCodeLifespan == time.Duration(0) { + config.AuthorizeCodeLifespan = schema.DefaultOpenIDConnectConfiguration.AuthorizeCodeLifespan + } + + if config.IDTokenLifespan == time.Duration(0) { + config.IDTokenLifespan = schema.DefaultOpenIDConnectConfiguration.IDTokenLifespan + } + + if config.RefreshTokenLifespan == time.Duration(0) { + config.RefreshTokenLifespan = schema.DefaultOpenIDConnectConfiguration.RefreshTokenLifespan + } + + if config.EnforcePKCE == "" { + config.EnforcePKCE = schema.DefaultOpenIDConnectConfiguration.EnforcePKCE } } diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go index 13d138bba..e087c589b 100644 --- a/internal/configuration/validator/identity_providers_test.go +++ b/internal/configuration/validator/identity_providers_test.go @@ -1,6 +1,9 @@ package validator import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" "errors" "fmt" "net/url" @@ -19,8 +22,7 @@ func TestShouldRaiseErrorWhenInvalidOIDCServerConfiguration(t *testing.T) { validator := schema.NewStructValidator() config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ - HMACSecret: "abc", - IssuerPrivateKey: "", + HMACSecret: "abc", }, } @@ -37,7 +39,7 @@ func TestShouldNotRaiseErrorWhenCORSEndpointsValid(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: "key-material", + IssuerPrivateKey: &rsa.PrivateKey{}, CORS: schema.OpenIDConnectCORSConfiguration{ Endpoints: []string{oidc.AuthorizationEndpoint, oidc.TokenEndpoint, oidc.IntrospectionEndpoint, oidc.RevocationEndpoint, oidc.UserinfoEndpoint}, }, @@ -60,7 +62,7 @@ func TestShouldRaiseErrorWhenCORSEndpointsNotValid(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: "key-material", + IssuerPrivateKey: &rsa.PrivateKey{}, CORS: schema.OpenIDConnectCORSConfiguration{ Endpoints: []string{oidc.AuthorizationEndpoint, oidc.TokenEndpoint, oidc.IntrospectionEndpoint, oidc.RevocationEndpoint, oidc.UserinfoEndpoint, "invalid_endpoint"}, }, @@ -85,7 +87,7 @@ func TestShouldRaiseErrorWhenOIDCPKCEEnforceValueInvalid(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: "key-material", + IssuerPrivateKey: &rsa.PrivateKey{}, EnforcePKCE: "invalid", }, } @@ -104,7 +106,7 @@ func TestShouldRaiseErrorWhenOIDCCORSOriginsHasInvalidValues(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: "key-material", + IssuerPrivateKey: &rsa.PrivateKey{}, CORS: schema.OpenIDConnectCORSConfiguration{ AllowedOrigins: utils.URLsFromStringSlice([]string{"https://example.com/", "https://site.example.com/subpath", "https://site.example.com?example=true", "*"}), AllowedOriginsFromClientRedirectURIs: true, @@ -140,7 +142,7 @@ func TestShouldRaiseErrorWhenOIDCServerNoClients(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: "key-material", + IssuerPrivateKey: &rsa.PrivateKey{}, }, } @@ -320,7 +322,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: "key-material", + IssuerPrivateKey: &rsa.PrivateKey{}, Clients: tc.Clients, }, } @@ -344,7 +346,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: "key-material", + IssuerPrivateKey: &rsa.PrivateKey{}, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "good_id", @@ -370,7 +372,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T) config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: "key-material", + IssuerPrivateKey: &rsa.PrivateKey{}, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "good_id", @@ -391,12 +393,66 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T) assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: client 'good_id': option 'grant_types' must only have the values 'implicit', 'refresh_token', 'authorization_code', 'password', 'client_credentials' but one option is configured as 'bad_grant_type'") } +func TestShouldNotErrorOnCertificateValid(t *testing.T) { + validator := schema.NewStructValidator() + config := &schema.IdentityProvidersConfiguration{ + OIDC: &schema.OpenIDConnectConfiguration{ + HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", + IssuerCertificateChain: mustParseX509CertificateChain(testCert1), + IssuerPrivateKey: mustParseRSAPrivateKey(testKey1), + Clients: []schema.OpenIDConnectClientConfiguration{ + { + ID: "good_id", + Secret: "good_secret", + Policy: "two_factor", + RedirectURIs: []string{ + "https://google.com/callback", + }, + }, + }, + }, + } + + ValidateIdentityProviders(config, validator) + + assert.Len(t, validator.Warnings(), 0) + assert.Len(t, validator.Errors(), 0) +} + +func TestShouldRaiseErrorOnCertificateNotValid(t *testing.T) { + validator := schema.NewStructValidator() + config := &schema.IdentityProvidersConfiguration{ + OIDC: &schema.OpenIDConnectConfiguration{ + HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", + IssuerCertificateChain: mustParseX509CertificateChain(testCert1), + IssuerPrivateKey: mustParseRSAPrivateKey(testKey2), + Clients: []schema.OpenIDConnectClientConfiguration{ + { + ID: "good_id", + Secret: "good_secret", + Policy: "two_factor", + RedirectURIs: []string{ + "https://google.com/callback", + }, + }, + }, + }, + } + + ValidateIdentityProviders(config, validator) + + assert.Len(t, validator.Warnings(), 0) + require.Len(t, validator.Errors(), 1) + + assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'issuer_private_key' does not appear to be the private key the certificate provided by option 'issuer_certificate_chain'") +} + func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadResponseModes(t *testing.T) { validator := schema.NewStructValidator() config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: "key-material", + IssuerPrivateKey: &rsa.PrivateKey{}, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "good_id", @@ -422,7 +478,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadUserinfoAlg(t *testing.T config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: "key-material", + IssuerPrivateKey: &rsa.PrivateKey{}, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "good_id", @@ -448,7 +504,7 @@ func TestValidateIdentityProvidersShouldRaiseWarningOnSecurityIssue(t *testing.T config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "abc", - IssuerPrivateKey: "abc", + IssuerPrivateKey: &rsa.PrivateKey{}, MinimumParameterEntropy: 1, Clients: []schema.OpenIDConnectClientConfiguration{ { @@ -476,7 +532,7 @@ func TestValidateIdentityProvidersShouldRaiseErrorsOnInvalidClientTypes(t *testi config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "hmac1", - IssuerPrivateKey: "key2", + IssuerPrivateKey: &rsa.PrivateKey{}, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-with-invalid-secret", @@ -514,7 +570,7 @@ func TestValidateIdentityProvidersShouldNotRaiseErrorsOnValidPublicClients(t *te config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "hmac1", - IssuerPrivateKey: "key2", + IssuerPrivateKey: &rsa.PrivateKey{}, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "installed-app-client", @@ -555,7 +611,7 @@ func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: "../../../README.md", + IssuerPrivateKey: &rsa.PrivateKey{}, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "a-client", @@ -691,3 +747,111 @@ func TestValidateOIDCClientRedirectURIsSupportingPrivateUseURISchemes(t *testing }) }) } + +var ( + testCert1 = ` +-----BEGIN CERTIFICATE----- +MIIC5jCCAc6gAwIBAgIRAJZ+6KrHw95zIDgm2arCTCgwDQYJKoZIhvcNAQELBQAw +EzERMA8GA1UEChMIQXV0aGVsaWEwHhcNMjIwOTA4MDIyNDQyWhcNMjMwOTA4MDIy +NDQyWjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMAE7muDAJtLsV3WgOpjrZ1JD1RlhuSOa3V+4zo2NYFQSdZW18SZ +fYYgUwLOleEy3VQ3N9MEFh/rWNHYHdsBjDvz/Q1EzAlXqthGd0Sic/UDYtrahrko +jCSkZCQ5YVO9ivMRth6XdUlu7RHVYY3aSOWPx2wiw9cdN+e4p73W6KwyzT7ezbUD +0Nng0Z7CNQTLHv3LBsLUODc4aVOvp2B4aAaw6cn990buKMvUuo2ge9gh0c5gIOM5 +dU7xOGAt7RzwCIHnG4CGAWPFuuS215ZeelgQr/9/fhtzDqSuBZw5f10vXnAyBwei +vN6Kffj2RXB+koFwBguT84A6cfmxWllGNF0CAwEAAaM1MDMwDgYDVR0PAQH/BAQD +AgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcN +AQELBQADggEBAFvORjj7RGoIc3q0fv6QjuncZ0Mu1/24O0smCr6tq5d6RQBRpb1M +jEsbTMLZErrHbyw/DWC75eJhW6T+6HiVTo6brBXkmDL+QGkLgRNOkZla6cnmIpmL +bf9iPmmcThscQERgYZzNg19zqK8JAQU/6PgU/N6OXTL/mQQoB972ET9dUl7lGx1Q +2l8XBe8t4QTp4t1xd3c4azxWvFNpzWBjC5eBWiVHLJmFXr4xpcnPFYFETOkvEqwt +pMQ2x895BoLrep6b+g0xeF4pmmIQwA9KrUVr++gpYaRzytaOIYwcIPMzt9iLWKQe +6ZSOrTVi8pPugYXp+LhVk/WI7r8EWtyADu0= +-----END CERTIFICATE-----` + + testKey1 = ` +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwATua4MAm0uxXdaA6mOtnUkPVGWG5I5rdX7jOjY1gVBJ1lbX +xJl9hiBTAs6V4TLdVDc30wQWH+tY0dgd2wGMO/P9DUTMCVeq2EZ3RKJz9QNi2tqG +uSiMJKRkJDlhU72K8xG2Hpd1SW7tEdVhjdpI5Y/HbCLD1x0357invdborDLNPt7N +tQPQ2eDRnsI1BMse/csGwtQ4NzhpU6+nYHhoBrDpyf33Ru4oy9S6jaB72CHRzmAg +4zl1TvE4YC3tHPAIgecbgIYBY8W65LbXll56WBCv/39+G3MOpK4FnDl/XS9ecDIH +B6K83op9+PZFcH6SgXAGC5PzgDpx+bFaWUY0XQIDAQABAoIBAQClcdpHcglMxOwe +kRpkWdwWAAQgUJXoSbnW86wu1NRHBfmInyyrrSBVN3aunXbQITZIQIdt3kB94haW +P6KBt5Svd2saSqOOjSWb0SMkVOCaQ/+h19VqpcASNj4+Y94y+8ZD5ofHVfJtghDr +Y7H5OhHDEZ3e0xlwODGaCyUkUY4KBv/oIlILoh4phbDYHkZH8AzDnEiyVE1JAWlN +voAQysgSU7eEnNCi1S07jl5bY+MD3XpJkAfQsJYhqYT/qetStZ12PuXjpbIr3y53 +qjCrKeWTyDN+gOznyIGuiR6nvXeQAw/o9hZiah4RuHXTPs/3GAcRXcuMR0pbgJ+B +yfX6eLK1AoGBAPKkJKPYJD2NHukAelNbT2OgeDIgJmfOIvLa73/x2hXvWwa4VwIC +POuKXtT/a02J4pYMGlaKXfHgLmaW2HPObOIjpxqgRIswsiKS1AbaDtkWnhaS1/SJ +oZ7Fk8DdX+1QT4J/fj/2uxRT0GhXdMxDpK7ekpmRE+APPCGhmOMgmWszAoGBAMqX +Ts1RdGWgDxLi15rDqdqRBARJG7Om/xC2voXVmbAb4Q+QoNrNeiIAM2usuhrVuj5V +c16m9fxswRNYqQBYyShDi5wp5a8UjfqDpzJdku2bmrBaL+XVq8PY+oTK6KS3ss8U +CGQ8P6Phz5JGavn/nDMRZ4EwEWqbEMUqJAJlpmIvAoGAQ9Wj8LJyn0qew6FAkaFL +dpzcPZdDZW35009l+a0RvWQnXJ+Yo5UglvEeRgoKY6kS0cQccOlKDl8QWdn+NZIW +WrqA8y6vOwKoKoZGBIxd7k8mb0UqXtFDf/HYtuis8tmrAN7H2vYNo0czUphwrNKU +bdcHwSsQFWns87IL3iO1AIUCgYBzmBX8jOePPN6c9hXzVoVKEshp8ZT+0uBilwLq +tk/07lNiYDGH5woy8E5mt62QtjaIbpVfgoCEwUEBWutDKWXNtYypVDabyWyhbhEu +abn2HX0L9smxqFNTcjCvKF/J7I74HQQUvVPKnIOlgMx1TOXBNcMLMXQekc/lz/+v +5nQjPQKBgQDjdJABeiy9tU0tzLWUVc5QoQKnlfSJoFLis46REb1yHwU9OjTc05Wx +5lAXdTjDmnelDdGWNWHjWOiKSkTxhvQD3jXriI5y8Sdxe3zS3ikYvbMbi97GJz0O +5oyNJo6/froW1dLkJJWR8hg2PQbtoOo6l9HHSd91BnJJ4qFbq9ZrXQ== +-----END RSA PRIVATE KEY-----` + + testKey2 = ` +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA6z1LOg1ZCqb0lytXWZ+MRBpMHEXOoTOLYgfZXt1IYyE3Z758 +cyalk0NYQhY5cZDsXPYWPvAHiPMUxutWkoxFwby56S+AbIMa3/Is+ILrHRJs8Exn +ZkpyrYFxPX12app2kErdmAkHSx0Z5/kuXiz96PHs8S8/ZbyZolLHzdfLtSzjvRm5 +Zue5iFzsf19NJz5CIBfv8g5lRwtE8wNJoRSpn1xq7fqfuA0weDNFPzjlNWRLy6aa +rK7qJexRkmkCs4sLgyl+9NODYJpvmN8E1yhyC27E0joI6rBFVW7Ihv+cSPCdDzGp +EWe81x3AeqAa3mjVqkiq4u4Z2i8JDgBaPboqJwIDAQABAoIBAAFdLZ58jVOefDSU +L8F5R1rtvBs93GDa56f926jNJ6pLewLC+/2+757W+SAI+PRLntM7Kg3bXm/Q2QH+ +Q1Y+MflZmspbWCdI61L5GIGoYKyeers59i+FpvySj5GHtLQRiTZ0+Kv1AXHSDWBm +9XneUOqU3IbZe0ifu1RRno72/VtjkGXbW8Mkkw+ohyGbIeTx/0/JQ6sSNZTT3Vk7 +8i4IXptq3HSF0/vqZuah8rShoeNq72pD1YLM9YPdL5by1QkDLnqATDiCpLBTCaNV +I8sqYEun+HYbQzBj8ZACG2JVZpEEidONWQHw5BPWO95DSZYrVnEkuCqeH+u5vYt7 +CHuJ3AECgYEA+W3v5z+j91w1VPHS0VB3SCDMouycAMIUnJPAbt+0LPP0scUFsBGE +hPAKddC54pmMZRQ2KIwBKiyWfCrJ8Xz8Yogn7fJgmwTHidJBr2WQpIEkNGlK3Dzi +jXL2sh0yC7sHvn0DqiQ79l/e7yRbSnv2wrTJEczOOH2haD7/tBRyCYECgYEA8W+q +E9YyGvEltnPFaOxofNZ8LHVcZSsQI5b6fc0iE7fjxFqeXPXEwGSOTwqQLQRiHn9b +CfPmIG4Vhyq0otVmlPvUnfBZ2OK+tl5X2/mQFO3ROMdvpi0KYa994uqfJdSTaqLn +jjoKFB906UFHnDQDLZUNiV1WwnkTglgLc+xrd6cCgYEAqqthyv6NyBTM3Tm2gcio +Ra9Dtntl51LlXZnvwy3IkDXBCd6BHM9vuLKyxZiziGx+Vy90O1xI872cnot8sINQ +Am+dur/tAEVN72zxyv0Y8qb2yfH96iKy9gxi5s75TnOEQgAygLnYWaWR2lorKRUX +bHTdXBOiS58S0UzCFEslGIECgYBqkO4SKWYeTDhoKvuEj2yjRYyzlu28XeCWxOo1 +otiauX0YSyNBRt2cSgYiTzhKFng0m+QUJYp63/wymB/5C5Zmxi0XtWIDADpLhqLj +HmmBQ2Mo26alQ5YkffBju0mZyhVzaQop1eZi8WuKFV1FThPlB7hc3E0SM5zv2Grd +tQnOWwKBgQC40yZY0PcjuILhy+sIc0Wvh7LUA7taSdTye149kRvbvsCDN7Jh75lM +USjhLXY0Nld2zBm9r8wMb81mXH29uvD+tDqqsICvyuKlA/tyzXR+QTr7dCVKVwu0 +1YjCJ36UpTsLre2f8nOSLtNmRfDPtbOE2mkOoO9dD9UU0XZwnvn9xw== +-----END RSA PRIVATE KEY-----` +) + +func mustParseRSAPrivateKey(data string) *rsa.PrivateKey { + block, _ := pem.Decode([]byte(data)) + if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { + panic("not pem encoded") + } + + if block.Type != "RSA PRIVATE KEY" { + panic("not private key") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + panic(err) + } + + return key +} + +func mustParseX509CertificateChain(data string) schema.X509CertificateChain { + chain, err := schema.NewX509CertificateChain(data) + + if err != nil { + panic(err) + } + + return *chain +} diff --git a/internal/handlers/handler_oidc_userinfo.go b/internal/handlers/handler_oidc_userinfo.go index 0e4c59143..99d3a6012 100644 --- a/internal/handlers/handler_oidc_userinfo.go +++ b/internal/handlers/handler_oidc_userinfo.go @@ -9,6 +9,7 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/token/jwt" "github.com/pkg/errors" + "github.com/valyala/fasthttp" "github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/model" @@ -35,7 +36,7 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, ctx.Logger.Errorf("UserInfo Request failed with error: %+v", rfc) if rfc.StatusCode() == http.StatusUnauthorized { - rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer error="%s",error_description="%s"`, rfc.ErrorField, rfc.GetDescription())) + rw.Header().Set(fasthttp.HeaderWWWAuthenticate, fmt.Sprintf(`Bearer error="%s",error_description="%s"`, rfc.ErrorField, rfc.GetDescription())) } ctx.Providers.OpenIDConnect.WriteError(rw, req, err) @@ -90,9 +91,7 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, claims["aud"] = audience - var ( - keyID, token string - ) + var token string ctx.Logger.Tracef("UserInfo Response with id '%s' on client with id '%s' is being sent with the following claims: %+v", requester.GetID(), clientID, claims) @@ -109,14 +108,8 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, claims["jti"] = jti.String() claims["iat"] = time.Now().Unix() - if keyID, err = ctx.Providers.OpenIDConnect.KeyManager.Strategy().GetPublicKeyID(req.Context()); err != nil { - ctx.Providers.OpenIDConnect.WriteError(rw, req, fosite.ErrServerError.WithHintf("Could not find the active JWK.")) - - return - } - headers := &jwt.Headers{ - Extra: map[string]interface{}{"kid": keyID}, + Extra: map[string]any{"kid": ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID()}, } if token, _, err = ctx.Providers.OpenIDConnect.KeyManager.Strategy().Generate(req.Context(), claims, headers); err != nil { diff --git a/internal/logging/logger.go b/internal/logging/logger.go index 761c8e646..3c5a549a4 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -37,8 +37,10 @@ func LoggerCtxPrintf(level logrus.Level) (logger *CtxPrintfLogger) { func InitializeLogger(config schema.LogConfiguration, log bool) error { setLevelStr(config.Level, log) - callerLevels := []logrus.Level{} + var callerLevels []logrus.Level + stackLevels := []logrus.Level{logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel} + logrus.AddHook(logrus_stack.NewHook(callerLevels, stackLevels)) if config.Format == logFormatJSON { diff --git a/internal/oidc/keys.go b/internal/oidc/keys.go index d5351061d..acbfc17d4 100644 --- a/internal/oidc/keys.go +++ b/internal/oidc/keys.go @@ -9,19 +9,17 @@ import ( "strings" "github.com/ory/fosite/token/jwt" - jose "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2" "github.com/authelia/authelia/v4/internal/configuration/schema" - "github.com/authelia/authelia/v4/internal/utils" ) // NewKeyManagerWithConfiguration when provided a schema.OpenIDConnectConfiguration creates a new KeyManager and adds an // initial key to the manager. -func NewKeyManagerWithConfiguration(configuration *schema.OpenIDConnectConfiguration) (manager *KeyManager, err error) { +func NewKeyManagerWithConfiguration(config *schema.OpenIDConnectConfiguration) (manager *KeyManager, err error) { manager = NewKeyManager() - _, _, err = manager.AddActivePrivateKeyData(configuration.IssuerPrivateKey) - if err != nil { + if _, err = manager.AddActiveJWK(config.IssuerCertificateChain, config.IssuerPrivateKey); err != nil { return nil, err } @@ -30,174 +28,152 @@ func NewKeyManagerWithConfiguration(configuration *schema.OpenIDConnectConfigura // NewKeyManager creates a new empty KeyManager. func NewKeyManager() (manager *KeyManager) { - manager = new(KeyManager) - manager.keys = map[string]*rsa.PrivateKey{} - manager.keySet = new(jose.JSONWebKeySet) - - return manager + return &KeyManager{ + jwks: &jose.JSONWebKeySet{}, + } } -// Strategy returns the RS256JWTStrategy. -func (m *KeyManager) Strategy() (strategy *RS256JWTStrategy) { - return m.strategy +// Strategy returns the fosite jwt.JWTStrategy. +func (m *KeyManager) Strategy() (strategy jwt.JWTStrategy) { + if m.jwk == nil { + return nil + } + + return m.jwk.Strategy() } // GetKeySet returns the joseJSONWebKeySet containing the rsa.PublicKey types. -func (m *KeyManager) GetKeySet() (keySet *jose.JSONWebKeySet) { - return m.keySet +func (m *KeyManager) GetKeySet() (jwks *jose.JSONWebKeySet) { + return m.jwks } -// GetActiveWebKey obtains the currently active jose.JSONWebKey. -func (m *KeyManager) GetActiveWebKey() (webKey *jose.JSONWebKey, err error) { - webKeys := m.keySet.Key(m.activeKeyID) - if len(webKeys) == 1 { - return &webKeys[0], nil +// GetActiveJWK obtains the currently active jose.JSONWebKey. +func (m *KeyManager) GetActiveJWK() (jwk *jose.JSONWebKey, err error) { + if m.jwks == nil || m.jwk == nil { + return nil, errors.New("could not obtain the active JWK from an improperly configured key manager") } - if len(webKeys) == 0 { + jwks := m.jwks.Key(m.jwk.id) + + if len(jwks) == 1 { + return &jwks[0], nil + } + + if len(jwks) == 0 { return nil, errors.New("could not find a key with the active key id") } - return &webKeys[0], errors.New("multiple keys with the same key id") + return nil, errors.New("multiple keys with the same key id") } // GetActiveKeyID returns the key id of the currently active key. func (m *KeyManager) GetActiveKeyID() (keyID string) { - return m.activeKeyID -} - -// GetActiveKey returns the rsa.PublicKey of the currently active key. -func (m *KeyManager) GetActiveKey() (key *rsa.PublicKey, err error) { - if key, ok := m.keys[m.activeKeyID]; ok { - return &key.PublicKey, nil + if m.jwk == nil { + return "" } - return nil, errors.New("failed to retrieve active public key") + return m.jwk.id } // GetActivePrivateKey returns the rsa.PrivateKey of the currently active key. func (m *KeyManager) GetActivePrivateKey() (key *rsa.PrivateKey, err error) { - if key, ok := m.keys[m.activeKeyID]; ok { - return key, nil + if m.jwk == nil { + return nil, errors.New("failed to retrieve active private key") } - return nil, errors.New("failed to retrieve active private key") + return m.jwk.key, nil } -// AddActivePrivateKeyData adds a rsa.PublicKey given the key in the PEM string format, then sets it to the active key. -func (m *KeyManager) AddActivePrivateKeyData(data string) (key *rsa.PrivateKey, webKey *jose.JSONWebKey, err error) { - ikey, err := utils.ParseX509FromPEM([]byte(data)) - if err != nil { - return nil, nil, err - } - - var ok bool - - if key, ok = ikey.(*rsa.PrivateKey); !ok { - return nil, nil, errors.New("key must be an RSA private key") - } - - webKey, err = m.AddActivePrivateKey(key) - - return key, webKey, err -} - -// AddActivePrivateKey adds a rsa.PublicKey, then sets it to the active key. -func (m *KeyManager) AddActivePrivateKey(key *rsa.PrivateKey) (webKey *jose.JSONWebKey, err error) { - wk := jose.JSONWebKey{ - Key: &key.PublicKey, - Algorithm: "RS256", - Use: "sig", - } - - keyID, err := wk.Thumbprint(crypto.SHA1) - if err != nil { +// AddActiveJWK is used to add a cert and key pair. +func (m *KeyManager) AddActiveJWK(chain schema.X509CertificateChain, key *rsa.PrivateKey) (jwk *JWK, err error) { + // TODO: Add a mutex when implementing key rotation to be utilized here and in methods which retrieve the JWK or JWKS. + if m.jwk, err = NewJWK(chain, key); err != nil { return nil, err } - strKeyID := strings.ToLower(fmt.Sprintf("%x", keyID)) - if len(strKeyID) >= 7 { - // Shorten the key if it's greater than 7 to a length of exactly 7. - strKeyID = strKeyID[0:6] - } + m.jwks.Keys = append(m.jwks.Keys, *m.jwk.JSONWebKey()) - if _, ok := m.keys[strKeyID]; ok { - return nil, fmt.Errorf("key id %s already exists", strKeyID) - } - - // TODO: Add Mutex here when implementing key rotation. - wk.KeyID = strKeyID - m.keySet.Keys = append(m.keySet.Keys, wk) - m.keys[strKeyID] = key - m.activeKeyID = strKeyID - - m.strategy, err = NewRS256JWTStrategy(wk.KeyID, key) - if err != nil { - return &wk, err - } - - return &wk, nil + return m.jwk, nil } -// NewRS256JWTStrategy returns a new RS256JWTStrategy. -func NewRS256JWTStrategy(id string, key *rsa.PrivateKey) (strategy *RS256JWTStrategy, err error) { - strategy = new(RS256JWTStrategy) - strategy.JWTStrategy = new(jwt.RS256JWTStrategy) +// JWTStrategy is a decorator struct for the fosite jwt.JWTStrategy. +type JWTStrategy struct { + jwt.JWTStrategy - strategy.SetKey(id, key) - - return strategy, nil -} - -// RS256JWTStrategy is a decorator struct for the fosite RS256JWTStrategy. -type RS256JWTStrategy struct { - JWTStrategy *jwt.RS256JWTStrategy - - keyID string + id string } // KeyID returns the key id. -func (s RS256JWTStrategy) KeyID() (id string) { - return s.keyID -} - -// SetKey sets the provided key id and key as the active key (this is what triggers fosite to use it). -func (s *RS256JWTStrategy) SetKey(id string, key *rsa.PrivateKey) { - s.keyID = id - s.JWTStrategy.PrivateKey = key -} - -// Hash is a decorator func for the underlying fosite RS256JWTStrategy. -func (s *RS256JWTStrategy) Hash(ctx context.Context, in []byte) ([]byte, error) { - return s.JWTStrategy.Hash(ctx, in) -} - -// GetSigningMethodLength is a decorator func for the underlying fosite RS256JWTStrategy. -func (s *RS256JWTStrategy) GetSigningMethodLength() int { - return s.JWTStrategy.GetSigningMethodLength() -} - -// GetSignature is a decorator func for the underlying fosite RS256JWTStrategy. -func (s *RS256JWTStrategy) GetSignature(ctx context.Context, token string) (string, error) { - return s.JWTStrategy.GetSignature(ctx, token) -} - -// Generate is a decorator func for the underlying fosite RS256JWTStrategy. -func (s *RS256JWTStrategy) Generate(ctx context.Context, claims jwt.MapClaims, header jwt.Mapper) (string, string, error) { - return s.JWTStrategy.Generate(ctx, claims, header) -} - -// Validate is a decorator func for the underlying fosite RS256JWTStrategy. -func (s *RS256JWTStrategy) Validate(ctx context.Context, token string) (string, error) { - return s.JWTStrategy.Validate(ctx, token) -} - -// Decode is a decorator func for the underlying fosite RS256JWTStrategy. -func (s *RS256JWTStrategy) Decode(ctx context.Context, token string) (*jwt.Token, error) { - return s.JWTStrategy.Decode(ctx, token) +func (s *JWTStrategy) KeyID() (id string) { + return s.id } // GetPublicKeyID is a decorator func for the underlying fosite RS256JWTStrategy. -func (s *RS256JWTStrategy) GetPublicKeyID(_ context.Context) (string, error) { - return s.keyID, nil +func (s *JWTStrategy) GetPublicKeyID(_ context.Context) (string, error) { + return s.id, nil +} + +// NewJWK creates a new JWK. +func NewJWK(chain schema.X509CertificateChain, key *rsa.PrivateKey) (j *JWK, err error) { + if key == nil { + return nil, fmt.Errorf("JWK is not properly initialized: missing key") + } + + j = &JWK{ + key: key, + chain: chain, + } + + jwk := &jose.JSONWebKey{ + Algorithm: "RS256", + Use: "sig", + Key: &key.PublicKey, + } + + var thumbprint []byte + + if thumbprint, err = jwk.Thumbprint(crypto.SHA1); err != nil { + return nil, fmt.Errorf("failed to calculate SHA1 thumbprint for certificate: %w", err) + } + + j.id = strings.ToLower(fmt.Sprintf("%x", thumbprint)) + + if len(j.id) >= 7 { + j.id = j.id[:6] + } + + if len(j.id) >= 7 { + j.id = j.id[:6] + } + + return j, nil +} + +// JWK is a utility wrapper for JSON Web Key's. +type JWK struct { + id string + key *rsa.PrivateKey + chain schema.X509CertificateChain +} + +// Strategy returns the relevant jwt.JWTStrategy for this JWT. +func (j *JWK) Strategy() (strategy jwt.JWTStrategy) { + return &JWTStrategy{id: j.id, JWTStrategy: &jwt.RS256JWTStrategy{PrivateKey: j.key}} +} + +// JSONWebKey returns the relevant *jose.JSONWebKey for this JWT. +func (j *JWK) JSONWebKey() (jwk *jose.JSONWebKey) { + jwk = &jose.JSONWebKey{ + Key: &j.key.PublicKey, + KeyID: j.id, + Algorithm: "RS256", + Use: "sig", + Certificates: j.chain.Certificates(), + } + + if len(jwk.Certificates) != 0 { + jwk.CertificateThumbprintSHA1, jwk.CertificateThumbprintSHA256 = j.chain.Thumbprint(crypto.SHA1), j.chain.Thumbprint(crypto.SHA256) + } + + return jwk } diff --git a/internal/oidc/keys_test.go b/internal/oidc/keys_test.go index 8ddfd71f8..1e15973ea 100644 --- a/internal/oidc/keys_test.go +++ b/internal/oidc/keys_test.go @@ -8,42 +8,37 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/authelia/authelia/v4/internal/configuration/schema" ) -func TestKeyManager_AddActiveKeyData(t *testing.T) { +func TestKeyManager_AddActiveJWK(t *testing.T) { manager := NewKeyManager() - assert.Nil(t, manager.strategy) + assert.Nil(t, manager.jwk) assert.Nil(t, manager.Strategy()) - key, wk, err := manager.AddActivePrivateKeyData(exampleIssuerPrivateKey) + j, err := manager.AddActiveJWK(schema.X509CertificateChain{}, mustParseRSAPrivateKey(exampleIssuerPrivateKey)) require.NoError(t, err) - require.NotNil(t, key) - require.NotNil(t, wk) - require.NotNil(t, manager.strategy) + require.NotNil(t, j) + require.NotNil(t, manager.jwk) require.NotNil(t, manager.Strategy()) - thumbprint, err := wk.Thumbprint(crypto.SHA1) + thumbprint, err := j.JSONWebKey().Thumbprint(crypto.SHA1) assert.NoError(t, err) - kid := strings.ToLower(fmt.Sprintf("%x", thumbprint)[0:6]) - assert.Equal(t, manager.activeKeyID, kid) - assert.Equal(t, kid, wk.KeyID) - assert.Len(t, manager.keys, 1) - assert.Len(t, manager.keySet.Keys, 1) - assert.Contains(t, manager.keys, kid) + kid := strings.ToLower(fmt.Sprintf("%x", thumbprint)[:6]) + assert.Equal(t, manager.jwk.id, kid) + assert.Equal(t, kid, j.JSONWebKey().KeyID) + assert.Len(t, manager.jwks.Keys, 1) - keys := manager.keySet.Key(kid) + keys := manager.jwks.Key(kid) assert.Equal(t, keys[0].KeyID, kid) privKey, err := manager.GetActivePrivateKey() assert.NoError(t, err) assert.NotNil(t, privKey) - pubKey, err := manager.GetActiveKey() - assert.NoError(t, err) - assert.NotNil(t, pubKey) - - webKey, err := manager.GetActiveWebKey() + webKey, err := manager.GetActiveJWK() assert.NoError(t, err) assert.NotNil(t, webKey) diff --git a/internal/oidc/provider.go b/internal/oidc/provider.go index 670d1e42c..5ca1993f4 100644 --- a/internal/oidc/provider.go +++ b/internal/oidc/provider.go @@ -14,17 +14,16 @@ import ( // NewOpenIDConnectProvider new-ups a OpenIDConnectProvider. func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, storageProvider storage.Provider) (provider OpenIDConnectProvider, err error) { - provider = OpenIDConnectProvider{ - Fosite: nil, - } - if config == nil { return provider, nil } - provider.Store = NewOpenIDConnectStore(config, storageProvider) + provider = OpenIDConnectProvider{ + Fosite: nil, + Store: NewOpenIDConnectStore(config, storageProvider), + } - composeConfiguration := &compose.Config{ + cconfig := &compose.Config{ AccessTokenLifespan: config.AccessTokenLifespan, AuthorizeCodeLifespan: config.AuthorizeCodeLifespan, IDTokenLifespan: config.IDTokenLifespan, @@ -50,19 +49,19 @@ func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, storage strategy := &compose.CommonStrategy{ CoreStrategy: compose.NewOAuth2HMACStrategy( - composeConfiguration, + cconfig, []byte(utils.HashSHA256FromString(config.HMACSecret)), nil, ), OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy( - composeConfiguration, + cconfig, key, ), JWTStrategy: provider.KeyManager.Strategy(), } provider.Fosite = compose.Compose( - composeConfiguration, + cconfig, provider.Store, strategy, PlainTextHasher{}, @@ -109,7 +108,7 @@ func (p OpenIDConnectProvider) Pairwise() bool { } // Write writes data with herodot.JSONWriter. -func (p OpenIDConnectProvider) Write(w http.ResponseWriter, r *http.Request, e interface{}, opts ...herodot.EncoderOptions) { +func (p OpenIDConnectProvider) Write(w http.ResponseWriter, r *http.Request, e any, opts ...herodot.EncoderOptions) { p.herodot.Write(w, r, e, opts...) } diff --git a/internal/oidc/provider_test.go b/internal/oidc/provider_test.go index f67a73797..b55c6f92a 100644 --- a/internal/oidc/provider_test.go +++ b/internal/oidc/provider_test.go @@ -1,6 +1,9 @@ package oidc import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" "net/url" "testing" @@ -20,17 +23,10 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_NotConfigured(t *testing assert.Nil(t, provider.Store) } -func TestOpenIDConnectProvider_NewOpenIDConnectProvider_BadIssuerKey(t *testing.T) { - _, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ - IssuerPrivateKey: "BAD KEY", - }, nil) - - assert.Error(t, err, "abc") -} - func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing.T) { provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ - IssuerPrivateKey: exampleIssuerPrivateKey, + IssuerCertificateChain: schema.X509CertificateChain{}, + IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), EnablePKCEPlainChallenge: true, HMACSecret: "asbdhaaskmdlkamdklasmdlkams", Clients: []schema.OpenIDConnectClientConfiguration{ @@ -63,8 +59,9 @@ func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *testing.T) { provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ - IssuerPrivateKey: exampleIssuerPrivateKey, - HMACSecret: "asbdhaaskmdlkamdklasmdlkams", + IssuerCertificateChain: schema.X509CertificateChain{}, + IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + HMACSecret: "asbdhaaskmdlkamdklasmdlkams", Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "a-client", @@ -102,8 +99,9 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *tes func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfiguration(t *testing.T) { provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ - IssuerPrivateKey: exampleIssuerPrivateKey, - HMACSecret: "asbdhaaskmdlkamdklasmdlkams", + IssuerCertificateChain: schema.X509CertificateChain{}, + IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + HMACSecret: "asbdhaaskmdlkamdklasmdlkams", Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "a-client", @@ -193,8 +191,9 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfiguration(t *testing.T) { provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ - IssuerPrivateKey: exampleIssuerPrivateKey, - HMACSecret: "asbdhaaskmdlkamdklasmdlkams", + IssuerCertificateChain: schema.X509CertificateChain{}, + IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + HMACSecret: "asbdhaaskmdlkamdklasmdlkams", Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "a-client", @@ -270,7 +269,8 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfigurationWithPlainPKCE(t *testing.T) { provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ - IssuerPrivateKey: exampleIssuerPrivateKey, + IssuerCertificateChain: schema.X509CertificateChain{}, + IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), HMACSecret: "asbdhaaskmdlkamdklasmdlkams", EnablePKCEPlainChallenge: true, Clients: []schema.OpenIDConnectClientConfiguration{ @@ -293,3 +293,21 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow assert.Equal(t, "S256", disco.CodeChallengeMethodsSupported[0]) assert.Equal(t, "plain", disco.CodeChallengeMethodsSupported[1]) } + +func mustParseRSAPrivateKey(data string) *rsa.PrivateKey { + block, _ := pem.Decode([]byte(data)) + if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { + panic("not pem encoded") + } + + if block.Type != "RSA PRIVATE KEY" { + panic("not private key") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + panic(err) + } + + return key +} diff --git a/internal/oidc/store_test.go b/internal/oidc/store_test.go index 412b81551..504af5e07 100644 --- a/internal/oidc/store_test.go +++ b/internal/oidc/store_test.go @@ -13,7 +13,8 @@ import ( func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) { s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{ - IssuerPrivateKey: exampleIssuerPrivateKey, + IssuerCertificateChain: schema.X509CertificateChain{}, + IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "myclient", @@ -44,7 +45,8 @@ func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) { func TestOpenIDConnectStore_GetInternalClient(t *testing.T) { s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{ - IssuerPrivateKey: exampleIssuerPrivateKey, + IssuerCertificateChain: schema.X509CertificateChain{}, + IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "myclient", @@ -76,8 +78,9 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) { } s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{ - IssuerPrivateKey: exampleIssuerPrivateKey, - Clients: []schema.OpenIDConnectClientConfiguration{c1}, + IssuerCertificateChain: schema.X509CertificateChain{}, + IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + Clients: []schema.OpenIDConnectClientConfiguration{c1}, }, nil) client, err := s.GetFullClient(c1.ID) @@ -103,8 +106,9 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) { } s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{ - IssuerPrivateKey: exampleIssuerPrivateKey, - Clients: []schema.OpenIDConnectClientConfiguration{c1}, + IssuerCertificateChain: schema.X509CertificateChain{}, + IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + Clients: []schema.OpenIDConnectClientConfiguration{c1}, }, nil) client, err := s.GetFullClient("another-client") @@ -114,7 +118,8 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) { func TestOpenIDConnectStore_IsValidClientID(t *testing.T) { s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{ - IssuerPrivateKey: exampleIssuerPrivateKey, + IssuerCertificateChain: schema.X509CertificateChain{}, + IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "myclient", diff --git a/internal/oidc/types.go b/internal/oidc/types.go index 02078b914..411a6fd94 100644 --- a/internal/oidc/types.go +++ b/internal/oidc/types.go @@ -1,7 +1,6 @@ package oidc import ( - "crypto/rsa" "time" "github.com/ory/fosite" @@ -21,21 +20,21 @@ func NewSession() (session *model.OpenIDSession) { return &model.OpenIDSession{ DefaultSession: &openid.DefaultSession{ Claims: &jwt.IDTokenClaims{ - Extra: map[string]interface{}{}, + Extra: map[string]any{}, }, Headers: &jwt.Headers{ - Extra: map[string]interface{}{}, + Extra: map[string]any{}, }, }, - Extra: map[string]interface{}{}, + Extra: map[string]any{}, } } // NewSessionWithAuthorizeRequest uses details from an AuthorizeRequester to generate an OpenIDSession. -func NewSessionWithAuthorizeRequest(issuer, kid, username string, amr []string, extra map[string]interface{}, +func NewSessionWithAuthorizeRequest(issuer, kid, username string, amr []string, extra map[string]any, authTime time.Time, consent *model.OAuth2ConsentSession, requester fosite.AuthorizeRequester) (session *model.OpenIDSession) { if extra == nil { - extra = make(map[string]interface{}) + extra = map[string]any{} } session = &model.OpenIDSession{ @@ -53,14 +52,14 @@ func NewSessionWithAuthorizeRequest(issuer, kid, username string, amr []string, AuthenticationMethodsReferences: amr, }, Headers: &jwt.Headers{ - Extra: map[string]interface{}{ + Extra: map[string]any{ "kid": kid, }, }, Subject: consent.Subject.UUID.String(), Username: username, }, - Extra: map[string]interface{}{}, + Extra: map[string]any{}, ClientID: requester.GetClient().GetID(), ChallengeID: consent.ChallengeID, } @@ -122,10 +121,8 @@ type Client struct { // KeyManager keeps track of all of the active/inactive rsa keys and provides them to services requiring them. // It additionally allows us to add keys for the purpose of key rotation in the future. type KeyManager struct { - activeKeyID string - keys map[string]*rsa.PrivateKey - keySet *jose.JSONWebKeySet - strategy *RS256JWTStrategy + jwk *JWK + jwks *jose.JSONWebKeySet } // PlainTextHasher implements the fosite.Hasher interface without an actual hashing algo. diff --git a/internal/utils/crypto.go b/internal/utils/crypto.go index e2aa80ee7..853abf511 100644 --- a/internal/utils/crypto.go +++ b/internal/utils/crypto.go @@ -182,7 +182,7 @@ func (ekb ECDSAKeyBuilder) Build() (interface{}, error) { } // ParseX509FromPEM parses PEM bytes and returns a PKCS key. -func ParseX509FromPEM(data []byte) (key interface{}, err error) { +func ParseX509FromPEM(data []byte) (key any, err error) { block, _ := pem.Decode(data) if block == nil { return nil, errors.New("failed to parse PEM block containing the key")