gowalk-cicd 1.0.86 → 1.0.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -0
- package/README.md +1 -0
- package/action/.daemux-version +1 -1
- package/action/scripts/cert_factory.py +6 -9
- package/action/scripts/prepare_signing.py +2 -2
- package/action/scripts/test_certificate_request.py +62 -0
- package/android-action/.daemux-version +1 -1
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -125,6 +125,7 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
125
125
|
`certificate-cap-policy` accepts only `fail`; a full cap requires registry reconciliation.
|
|
126
126
|
A certificate POST's HTTP 409 alone does not prove a full cap. `certificate_failure.py`
|
|
127
127
|
emits fixed reason/code evidence in `apple_certificate_rejected`; raw provider details stay private.
|
|
128
|
+
Certificate creation sends the complete PEM CSR in `csrContent`, including its framing and newlines.
|
|
128
129
|
- Keep preinstalled Android SDK/NDKs. Every Gradle test/build and SDK download runs
|
|
129
130
|
under `play_store_proxy.py --`, whose owned loopback relay gives Java an explicit
|
|
130
131
|
authenticated account exit without credentials in JVM options. The child HTTP_PROXY
|
package/README.md
CHANGED
|
@@ -852,6 +852,7 @@ common ones:
|
|
|
852
852
|
| `ai-metadata` | `false` to disable AI auto-fill of empty ASC metadata |
|
|
853
853
|
| `ai-metadata-model` | GitHub Models model id (default `openai/gpt-4o`) |
|
|
854
854
|
|
|
855
|
+
Certificate creation sends the complete PEM CSR in Apple's `csrContent` field, including its framing and newlines.
|
|
855
856
|
Certificate creation refusals emit `apple_certificate_rejected` with schema
|
|
856
857
|
`gowalk-cicd/apple-certificate-rejected.v1`, `operation`, `http_status`, `reason` and `error_codes`.
|
|
857
858
|
HTTP 409 is classified as `certificate_limit` only when Apple's returned detail identifies the limit;
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.87
|
|
@@ -35,11 +35,11 @@ def certificate_cap_policy() -> str:
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def generate_key_and_csr() -> tuple[rsa.RSAPrivateKey, bytes]:
|
|
38
|
-
"""Generate a fresh 2048-bit RSA key
|
|
38
|
+
"""Generate a fresh 2048-bit RSA key and complete PEM CSR.
|
|
39
39
|
|
|
40
40
|
Returns the private key (kept in memory; the caller serialises it
|
|
41
|
-
into the PKCS12) and the CSR
|
|
42
|
-
Apple's ``csrContent`` field.
|
|
41
|
+
into the PKCS12) and the PEM CSR, including its framing and newlines,
|
|
42
|
+
for Apple's ``csrContent`` field.
|
|
43
43
|
"""
|
|
44
44
|
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
45
45
|
csr = (
|
|
@@ -55,20 +55,17 @@ def generate_key_and_csr() -> tuple[rsa.RSAPrivateKey, bytes]:
|
|
|
55
55
|
.sign(private_key, hashes.SHA256())
|
|
56
56
|
)
|
|
57
57
|
csr_pem = csr.public_bytes(serialization.Encoding.PEM)
|
|
58
|
-
|
|
59
|
-
line for line in csr_pem.splitlines() if not line.startswith(b"-----")
|
|
60
|
-
)
|
|
61
|
-
return private_key, csr_payload
|
|
58
|
+
return private_key, csr_pem
|
|
62
59
|
|
|
63
60
|
|
|
64
|
-
def create_distribution_cert(token: str,
|
|
61
|
+
def create_distribution_cert(token: str, csr_pem: str) -> tuple[str, bytes]:
|
|
65
62
|
"""POST one CSR; a full team cap preserves every existing certificate."""
|
|
66
63
|
certificate_cap_policy()
|
|
67
64
|
body = {
|
|
68
65
|
"data": {
|
|
69
66
|
"type": "certificates",
|
|
70
67
|
"attributes": {
|
|
71
|
-
"csrContent":
|
|
68
|
+
"csrContent": csr_pem,
|
|
72
69
|
"certificateType": "DISTRIBUTION",
|
|
73
70
|
},
|
|
74
71
|
}
|
|
@@ -103,9 +103,9 @@ def _load_or_create_cert(
|
|
|
103
103
|
)
|
|
104
104
|
creds_store.invalidate_cache(creds_dir)
|
|
105
105
|
|
|
106
|
-
private_key,
|
|
106
|
+
private_key, csr_pem = cert_factory.generate_key_and_csr()
|
|
107
107
|
cert_id, cert_der = cert_factory.create_distribution_cert(
|
|
108
|
-
token,
|
|
108
|
+
token, csr_pem.decode("ascii")
|
|
109
109
|
)
|
|
110
110
|
password = creds_store.P12_PASSWORD
|
|
111
111
|
p12_bytes = cert_factory.serialize_p12(private_key, cert_der, password)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Verify Apple's PEM request boundary with real CSR, certificate and PKCS12 bytes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import datetime as dt
|
|
6
|
+
import json
|
|
7
|
+
import unittest
|
|
8
|
+
from contextlib import redirect_stdout
|
|
9
|
+
from io import StringIO
|
|
10
|
+
from unittest import mock
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
from cryptography import x509
|
|
14
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
15
|
+
from cryptography.hazmat.primitives.serialization import pkcs12
|
|
16
|
+
|
|
17
|
+
import cert_factory
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CertificateRequestTests(unittest.TestCase):
|
|
21
|
+
def test_generated_pem_has_a_valid_signature_and_matching_public_key(self):
|
|
22
|
+
key, content = cert_factory.generate_key_and_csr()
|
|
23
|
+
csr = x509.load_pem_x509_csr(content)
|
|
24
|
+
self.assertTrue(csr.is_signature_valid)
|
|
25
|
+
self.assertEqual(csr.public_key().public_numbers(), key.public_key().public_numbers())
|
|
26
|
+
|
|
27
|
+
def test_posted_pem_and_returned_certificate_preserve_the_signing_identity(self):
|
|
28
|
+
key, content = cert_factory.generate_key_and_csr()
|
|
29
|
+
received = []
|
|
30
|
+
|
|
31
|
+
def apple_post(method, path, token, *, json_body, allow_status):
|
|
32
|
+
self.assertEqual((method, path), ("POST", "/certificates"))
|
|
33
|
+
attributes = json_body["data"]["attributes"]
|
|
34
|
+
self.assertEqual(attributes["certificateType"], "DISTRIBUTION")
|
|
35
|
+
csr = x509.load_pem_x509_csr(attributes["csrContent"].encode("ascii"))
|
|
36
|
+
self.assertTrue(csr.is_signature_valid)
|
|
37
|
+
now = dt.datetime.now(dt.timezone.utc)
|
|
38
|
+
cert = (x509.CertificateBuilder().subject_name(csr.subject).issuer_name(csr.subject)
|
|
39
|
+
.public_key(csr.public_key()).serial_number(x509.random_serial_number())
|
|
40
|
+
.not_valid_before(now - dt.timedelta(minutes=1))
|
|
41
|
+
.not_valid_after(now + dt.timedelta(days=1)).sign(key, hashes.SHA256()))
|
|
42
|
+
received.append(cert)
|
|
43
|
+
response = requests.Response()
|
|
44
|
+
response.status_code = 201
|
|
45
|
+
response._content = json.dumps({"data": {"id": "TESTCERT", "attributes": {
|
|
46
|
+
"certificateContent": base64.b64encode(cert.public_bytes(serialization.Encoding.DER)).decode()
|
|
47
|
+
}}}).encode()
|
|
48
|
+
return response
|
|
49
|
+
|
|
50
|
+
with mock.patch.object(cert_factory, "request", side_effect=apple_post) as request:
|
|
51
|
+
with redirect_stdout(StringIO()):
|
|
52
|
+
cert_id, der = cert_factory.create_distribution_cert("test-token", content.decode("ascii"))
|
|
53
|
+
request.assert_called_once()
|
|
54
|
+
self.assertEqual(cert_id, "TESTCERT")
|
|
55
|
+
p12 = cert_factory.serialize_p12(key, der, "test-only-password")
|
|
56
|
+
restored_key, restored_cert, _ = pkcs12.load_key_and_certificates(p12, b"test-only-password")
|
|
57
|
+
self.assertEqual(restored_key.public_key().public_numbers(), key.public_key().public_numbers())
|
|
58
|
+
self.assertEqual(restored_cert.fingerprint(hashes.SHA256()), received[0].fingerprint(hashes.SHA256()))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
unittest.main()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.87
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.87
|