Skip to content

Commit 522e52e

Browse files
authored
Merge branch 'master' into removed-leases-revoke-wallet-instances-collection
2 parents 480bd7d + 580f782 commit 522e52e

File tree

5 files changed

+213
-179
lines changed

5 files changed

+213
-179
lines changed

apps/io-wallet-user-func/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# io-wallet-user-func
22

3+
## 4.1.7
4+
5+
### Patch Changes
6+
7+
- f78da99: Refactors the MobileAttestationService class
8+
39
## 4.1.6
410

511
### Patch Changes

apps/io-wallet-user-func/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "io-wallet-user-func",
3-
"version": "4.1.6",
3+
"version": "4.1.7",
44
"private": true,
55
"scripts": {
66
"build": "tsup-node",

apps/io-wallet-user-func/src/infra/mobile-attestation-service/android/index.ts

Lines changed: 63 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { X509Certificate } from "crypto";
44
import * as E from "fp-ts/Either";
55
import { flow, pipe } from "fp-ts/function";
66
import * as J from "fp-ts/Json";
7-
import * as RA from "fp-ts/lib/ReadonlyArray";
87
import * as S from "fp-ts/lib/string";
8+
import * as RA from "fp-ts/ReadonlyArray";
99
import * as TE from "fp-ts/TaskEither";
1010
import * as t from "io-ts";
1111
import { AndroidDeviceDetails } from "io-wallet-common/device-details";
@@ -26,97 +26,96 @@ const DeviceDetailsWithKey = t.type({
2626
hardwareKey: JwkPublicKey,
2727
});
2828

29+
export const parseAndroidAttestation = (data: Buffer) =>
30+
pipe(
31+
data.toString("utf-8"),
32+
S.split(","),
33+
RA.map((b64) =>
34+
E.tryCatch(
35+
() => new X509Certificate(base64ToPem(b64)),
36+
() => new AndroidAttestationError("Unable to decode X509 certificate"),
37+
),
38+
),
39+
RA.sequence(E.Applicative),
40+
);
41+
2942
export const validateAndroidAttestation = (
30-
data: Buffer,
43+
x509Chain: readonly X509Certificate[],
3144
nonce: NonEmptyString,
3245
bundleIdentifiers: string[],
3346
googlePublicKeys: string[],
3447
androidCrlUrl: string,
3548
httpRequestTimeout: number,
3649
): TE.TaskEither<Error | ValidationError, ValidatedAttestation> =>
3750
pipe(
38-
data.toString("utf-8"),
39-
S.split(","),
40-
RA.map(
41-
flow(base64ToPem, (cert) =>
42-
E.tryCatch(
43-
() => new X509Certificate(cert),
44-
() =>
45-
new AndroidAttestationError(`Unable to decode X509 certificate`),
46-
),
51+
getCrlFromUrl(androidCrlUrl, httpRequestTimeout),
52+
TE.chain((attestationCrl) =>
53+
TE.tryCatch(
54+
() =>
55+
verifyAttestation({
56+
attestationCrl,
57+
bundleIdentifiers,
58+
challenge: nonce,
59+
googlePublicKeys,
60+
x509Chain,
61+
}),
62+
E.toError,
4763
),
4864
),
49-
RA.sequence(E.Applicative),
50-
TE.fromEither,
51-
TE.chain((x509Chain) =>
52-
pipe(
53-
getCrlFromUrl(androidCrlUrl, httpRequestTimeout),
54-
TE.chain((attestationCrl) =>
55-
TE.tryCatch(
56-
() =>
57-
verifyAttestation({
58-
attestationCrl,
59-
bundleIdentifiers,
60-
challenge: nonce,
61-
googlePublicKeys,
62-
x509Chain,
63-
}),
64-
E.toError,
65+
TE.chain((attestationValidationResult) =>
66+
attestationValidationResult.success
67+
? TE.right(attestationValidationResult)
68+
: TE.left(
69+
new AndroidAttestationError(attestationValidationResult.reason),
6570
),
66-
),
67-
TE.chain((attestationValidationResult) =>
68-
attestationValidationResult.success
69-
? TE.right(attestationValidationResult)
70-
: TE.left(
71-
new AndroidAttestationError(attestationValidationResult.reason),
72-
),
73-
),
74-
TE.chainW(flow(parse(DeviceDetailsWithKey), TE.fromEither)),
75-
),
7671
),
72+
TE.chainW(flow(parse(DeviceDetailsWithKey), TE.fromEither)),
7773
);
7874

79-
export const validateAndroidAssertion = (
80-
integrityAssertion: NonEmptyString,
81-
hardwareSignature: NonEmptyString,
82-
clientData: string,
83-
hardwareKey: JwkPublicKey,
84-
bundleIdentifiers: string[],
85-
androidPlayStoreCertificateHash: string,
75+
export const parseGoogleAppCredentials = (
8676
googleAppCredentialsEncoded: string,
87-
androidPlayIntegrityUrl: string,
88-
allowDevelopmentEnvironment: boolean,
8977
) =>
9078
pipe(
9179
E.tryCatch(
9280
() => Buffer.from(googleAppCredentialsEncoded, "base64").toString(),
9381
E.toError,
9482
),
9583
E.chain(J.parse),
84+
E.chainW(parse(GoogleAppCredentials)),
9685
E.mapLeft(
9786
() =>
9887
new AndroidAssertionError(
9988
"Unable to parse Google App Credentials string",
10089
),
10190
),
102-
E.chainW(parse(GoogleAppCredentials, "Invalid Google App Credentials")),
103-
TE.fromEither,
104-
TE.chain((googleAppCredentials) =>
105-
TE.tryCatch(
106-
() =>
107-
verifyAssertion({
108-
allowDevelopmentEnvironment,
109-
androidPlayIntegrityUrl,
110-
androidPlayStoreCertificateHash,
111-
bundleIdentifiers,
112-
clientData,
113-
googleAppCredentials,
114-
hardwareKey,
115-
hardwareSignature,
116-
integrityAssertion,
117-
}),
118-
E.toError,
119-
),
91+
);
92+
93+
export const validateAndroidAssertion = (
94+
integrityAssertion: NonEmptyString,
95+
hardwareSignature: NonEmptyString,
96+
clientData: string,
97+
hardwareKey: JwkPublicKey,
98+
bundleIdentifiers: string[],
99+
androidPlayStoreCertificateHash: string,
100+
googleAppCredentials: GoogleAppCredentials,
101+
androidPlayIntegrityUrl: string,
102+
allowDevelopmentEnvironment: boolean,
103+
) =>
104+
pipe(
105+
TE.tryCatch(
106+
() =>
107+
verifyAssertion({
108+
allowDevelopmentEnvironment,
109+
androidPlayIntegrityUrl,
110+
androidPlayStoreCertificateHash,
111+
bundleIdentifiers,
112+
clientData,
113+
googleAppCredentials,
114+
hardwareKey,
115+
hardwareSignature,
116+
integrityAssertion,
117+
}),
118+
E.toError,
120119
),
121120
TE.chain((assertionValidationResult) =>
122121
assertionValidationResult.success

apps/io-wallet-user-func/src/infra/mobile-attestation-service/index.ts

Lines changed: 67 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ import { ValidationError } from "@pagopa/handler-kit";
22
import { FiscalCode, NonEmptyString } from "@pagopa/ts-commons/lib/strings";
33
import { createPublicKey } from "crypto";
44
import * as E from "fp-ts/Either";
5-
import { identity, pipe } from "fp-ts/function";
5+
import { pipe } from "fp-ts/function";
66
import * as J from "fp-ts/Json";
7-
import { Separated } from "fp-ts/lib/Separated";
87
import * as O from "fp-ts/Option";
98
import * as RA from "fp-ts/ReadonlyArray";
10-
import * as T from "fp-ts/Task";
119
import * as TE from "fp-ts/TaskEither";
1210
import { JwkPublicKey } from "io-wallet-common/jwk";
1311
import { calculateJwkThumbprint } from "jose";
@@ -21,10 +19,17 @@ import {
2119
} from "@/attestation-service";
2220

2321
import {
22+
parseAndroidAttestation,
23+
parseGoogleAppCredentials,
2424
validateAndroidAssertion,
2525
validateAndroidAttestation,
2626
} from "./android";
27-
import { validateiOSAssertion, validateiOSAttestation } from "./ios";
27+
import {
28+
parseIosAssertion,
29+
parseIosAttestation,
30+
validateiOSAssertion,
31+
validateiOSAttestation,
32+
} from "./ios";
2833

2934
export class IntegrityCheckError extends Error {
3035
name = "IntegrityCheckError";
@@ -36,18 +41,6 @@ export class IntegrityCheckError extends Error {
3641
const toIntegrityCheckError = (e: Error | ValidationError): Error =>
3742
e instanceof ValidationError ? new IntegrityCheckError(e.violations) : e;
3843

39-
const getErrorsOrFirstValidValue = (
40-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41-
validated: Separated<readonly Error[], readonly any[]>,
42-
) =>
43-
pipe(
44-
validated.right,
45-
RA.head,
46-
E.fromOption(
47-
() => new IntegrityCheckError(validated.left.map((el) => el.message)),
48-
),
49-
);
50-
5144
export class MobileAttestationService implements AttestationService {
5245
#configuration: AttestationServiceConfiguration;
5346

@@ -87,41 +80,55 @@ export class MobileAttestationService implements AttestationService {
8780
TE.tryCatch(() => calculateJwkThumbprint(jwk, "sha256"), E.toError),
8881
TE.chainEitherKW((jwk_thumbprint) =>
8982
pipe(
90-
{
91-
challenge: nonce,
92-
jwk_thumbprint,
93-
},
83+
{ challenge: nonce, jwk_thumbprint },
9484
J.stringify,
9585
E.mapLeft(() => new ValidationError(["Unable to create clientData"])),
9686
),
9787
),
9888
TE.chainW((clientData) =>
9989
pipe(
100-
[
90+
parseIosAssertion({
91+
hardwareSignature,
92+
integrityAssertion,
93+
}),
94+
TE.fromEither,
95+
TE.chainW((decodedAssertion) =>
10196
validateiOSAssertion(
102-
integrityAssertion,
103-
hardwareSignature,
97+
decodedAssertion,
10498
clientData,
10599
hardwareKey,
106100
signCount,
107101
this.#configuration.iosBundleIdentifiers,
108102
this.#configuration.iOsTeamIdentifier,
109103
this.#configuration.skipSignatureValidation,
110104
),
111-
validateAndroidAssertion(
112-
integrityAssertion,
113-
hardwareSignature,
114-
clientData,
115-
hardwareKey,
116-
this.#configuration.androidBundleIdentifiers,
117-
this.#configuration.androidPlayStoreCertificateHash,
118-
this.#configuration.googleAppCredentialsEncoded,
119-
this.#configuration.androidPlayIntegrityUrl,
120-
this.allowDevelopmentEnvironmentForUser(user),
105+
),
106+
TE.orElseW((iosErr) =>
107+
pipe(
108+
parseGoogleAppCredentials(
109+
this.#configuration.googleAppCredentialsEncoded,
110+
),
111+
TE.fromEither,
112+
TE.chain((googleAppCredentials) =>
113+
validateAndroidAssertion(
114+
integrityAssertion,
115+
hardwareSignature,
116+
clientData,
117+
hardwareKey,
118+
this.#configuration.androidBundleIdentifiers,
119+
this.#configuration.androidPlayStoreCertificateHash,
120+
googleAppCredentials,
121+
this.#configuration.androidPlayIntegrityUrl,
122+
this.allowDevelopmentEnvironmentForUser(user),
123+
),
124+
),
125+
TE.orElseW((androidErr) =>
126+
TE.left(
127+
new IntegrityCheckError([iosErr.message, androidErr.message]),
128+
),
129+
),
121130
),
122-
],
123-
RA.wilt(T.ApplicativePar)(identity),
124-
T.map(getErrorsOrFirstValidValue),
131+
),
125132
),
126133
),
127134
);
@@ -140,32 +147,44 @@ export class MobileAttestationService implements AttestationService {
140147
TE.fromEither,
141148
TE.chainW((data) =>
142149
pipe(
143-
[
150+
parseIosAttestation(data),
151+
TE.fromEither,
152+
TE.chainW((decoded) =>
144153
validateiOSAttestation(
145-
data,
154+
decoded,
146155
nonce,
147156
hardwareKeyTag,
148157
this.#configuration.iosBundleIdentifiers,
149158
this.#configuration.iOsTeamIdentifier,
150159
this.#configuration.appleRootCertificate,
151160
this.allowDevelopmentEnvironmentForUser(user),
152161
),
162+
),
163+
TE.orElseW((iosErr) =>
153164
pipe(
154-
validateAndroidAttestation(
155-
data,
156-
nonce,
157-
this.#configuration.androidBundleIdentifiers,
158-
this.#configuration.googlePublicKeys,
159-
this.#configuration.androidCrlUrl,
160-
this.#configuration.httpRequestTimeout,
165+
parseAndroidAttestation(data),
166+
TE.fromEither,
167+
TE.chainW((x509Chain) =>
168+
validateAndroidAttestation(
169+
x509Chain,
170+
nonce,
171+
this.#configuration.androidBundleIdentifiers,
172+
this.#configuration.googlePublicKeys,
173+
this.#configuration.androidCrlUrl,
174+
this.#configuration.httpRequestTimeout,
175+
),
161176
),
162177
TE.mapLeft(toIntegrityCheckError),
178+
TE.orElseW((androidErr) =>
179+
TE.left(
180+
new IntegrityCheckError([iosErr.message, androidErr.message]),
181+
),
182+
),
163183
),
164-
],
165-
RA.wilt(T.ApplicativeSeq)(identity),
166-
T.map(getErrorsOrFirstValidValue),
184+
),
167185
),
168186
),
169187
);
170188
}
189+
171190
export { ValidatedAttestation };

0 commit comments

Comments
 (0)