Dear all,

The below changeset extends rpki-client(8) to validate and emit
Autonomous System Provider Authorizations (ASPAs) in JSON format for
consumption in other routing stacks.

The ASPA CMS protected content type is specified here:
https://datatracker.ietf.org/doc/html/draft-ietf-sidrops-aspa-profile

How to verify BGP UPDATES using ASPA objects is described here:
https://datatracker.ietf.org/doc/html/draft-ietf-sidrops-aspa-verification
(Note: aspa-profile and aspa-verification really are two different beasts)

A test suite to generate conforming and non-conforming ASPA objects is
available at: https://github.com/benmaddison/rpki-aspa-test-data
Another testbed was outlined here:
https://mailarchive.ietf.org/arch/msg/sidrops/DvRqMbOXkhiBDdUCTJU89L4q6LY

The rpki-client(8) '-j' (/var/db/rpki-client/json) output is formatted
such that transformation into draft-ietf-sidrops-8210bis § 5.12
RPKI-To-Router PDUs should be easy. However, for future direct
disk-to-bgpd(8) dissemination of ASPA objects; we probably can come up
with something more efficient :-)

Example output from "rpki-client -f":

    File: 
rpki.example.net/rpki/TA/ca-case-multi-afi-true/2338bd30cba1ff30fe3cf930824f39b45569d458de356729cb0121400ee3616a.asa
    Hash identifier: P+IG54iYS6VLlZmnIacSks0bZwHxItkIHPt5YjI4y58=
    Subject key identifier: 
CB:B8:19:D8:E0:FD:8E:0E:10:83:D6:11:6B:2C:CD:92:28:8F:F8:C9
    Certificate serial: 01
    Authority key identifier: 
3C:E8:78:93:CE:9C:7F:41:53:0D:D0:E8:BB:CA:B0:03:7F:69:43:74
    Authority info access: 
rsync://rpki.example.net/rpki/TA/ca-case-multi-afi-true.cer
    Customer AS: 65004
    Provider Set:
        1: AS: 65005
        2: AS: 65006 (IPv4 only)
        3: AS: 65007 (IPv6 only)
    Validation: OK

OK? Feedback?

Kind regards,

Job

Index: Makefile
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/Makefile,v
retrieving revision 1.25
diff -u -p -r1.25 Makefile
--- Makefile    9 May 2022 17:02:34 -0000       1.25
+++ Makefile    13 Aug 2022 00:24:24 -0000
@@ -1,8 +1,8 @@
 #      $OpenBSD: Makefile,v 1.25 2022/05/09 17:02:34 job Exp $
 
 PROG=  rpki-client
-SRCS=  as.c cert.c cms.c crl.c encoding.c filemode.c gbr.c http.c io.c ip.c \
-       log.c main.c mft.c mkdir.c output.c output-bgpd.c output-bird.c \
+SRCS=  as.c aspa.c cert.c cms.c crl.c encoding.c filemode.c gbr.c http.c io.c \
+       ip.c log.c main.c mft.c mkdir.c output.c output-bgpd.c output-bird.c \
        output-csv.c output-json.c parser.c print.c repo.c roa.c rrdp.c \
        rrdp_delta.c rrdp_notification.c rrdp_snapshot.c rrdp_util.c \
        rsc.c rsync.c tal.c validate.c x509.c
Index: aspa.c
===================================================================
RCS file: aspa.c
diff -N aspa.c
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ aspa.c      13 Aug 2022 00:24:24 -0000
@@ -0,0 +1,447 @@
+/*     $OpenBSD: aspa.c,v 1.16 2022/05/11 21:19:06 job Exp $ */
+/*
+ * Copyright (c) 2022 Job Snijders <j...@fastly.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <err.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <openssl/asn1.h>
+#include <openssl/asn1t.h>
+#include <openssl/stack.h>
+#include <openssl/safestack.h>
+#include <openssl/x509.h>
+
+#include "extern.h"
+
+/*
+ * Parse results and data of the ASPA object.
+ */
+struct parse {
+       const char       *fn; /* ASPA file name */
+       struct aspa      *res; /* results */
+};
+
+extern ASN1_OBJECT     *aspa_oid;
+
+/*
+ * Types and templates for ASPA eContent draft-ietf-sidrops-aspa-profile-08
+ */
+
+typedef struct {
+       ASN1_INTEGER            *providerASID;
+       ASN1_OCTET_STRING       *afiLimit;
+} ProviderAS;
+
+DECLARE_STACK_OF(ProviderAS);
+
+#ifndef DEFINE_STACK_OF
+#define sk_ProviderAS_num(sk)          SKM_sk_num(ProviderAS, (sk))
+#define sk_ProviderAS_value(sk, i)     SKM_sk_value(ProviderAS, (sk), (i))
+#endif
+
+ASN1_SEQUENCE(ProviderAS) = {
+       ASN1_SIMPLE(ProviderAS, providerASID, ASN1_INTEGER),
+       ASN1_OPT(ProviderAS, afiLimit, ASN1_OCTET_STRING),
+} ASN1_SEQUENCE_END(ProviderAS);
+
+typedef struct {
+       ASN1_INTEGER            *version;
+       ASN1_INTEGER            *customerASID;
+       STACK_OF(ProviderAS)    *providers;
+} ASProviderAttestation;
+
+ASN1_SEQUENCE(ASProviderAttestation) = {
+       ASN1_IMP_OPT(ASProviderAttestation, version, ASN1_INTEGER, 0),
+       ASN1_SIMPLE(ASProviderAttestation, customerASID, ASN1_INTEGER),
+       ASN1_SEQUENCE_OF(ASProviderAttestation, providers, ProviderAS),
+} ASN1_SEQUENCE_END(ASProviderAttestation);
+
+DECLARE_ASN1_FUNCTIONS(ASProviderAttestation);
+IMPLEMENT_ASN1_FUNCTIONS(ASProviderAttestation);
+
+/*
+ * Parse the ProviderASSet sequence.
+ * Return zero on failure, non-zero on success.
+ */
+static int
+aspa_parse_providers(struct parse *p, const STACK_OF(ProviderAS) *providers)
+{
+       ProviderAS              *pa;
+       struct aspa_provider     aspa_p;
+       size_t                   providersz, i, j;
+
+       memset(&aspa_p, 0, sizeof(struct aspa_provider));
+
+       if ((providersz = sk_ProviderAS_num(providers)) == 0) {
+               warnx("%s: ASPA: ProviderASSet needs at least one entry",
+                   p->fn);
+               return 0;
+       }
+
+       if (providersz >= MAX_ASPA_PROVIDERS) {
+               warnx("%s: ASPA: too many providers (more than %d)", p->fn,
+                   MAX_ASPA_PROVIDERS);
+               return 0;
+       }
+
+       p->res->providers = calloc(providersz, sizeof(struct aspa_provider));
+       if (p->res->providers == NULL)
+               err(1, NULL);
+
+       for (i = 0; i < providersz; i++) {
+               pa = sk_ProviderAS_value(providers, i);
+
+               if (!as_id_parse(pa->providerASID, &aspa_p.as)) {
+                       warnx("%s: ASPA: malformed ProviderAS", p->fn);
+                       return 0;
+               }
+
+               if (p->res->custasid == aspa_p.as) {
+                       warnx("%s: ASPA: CustomerASID can't also be Provider",
+                           p->fn);
+                       return 0;
+               }
+
+               if (i > 0)
+                       if (p->res->providers[i - 1].as > aspa_p.as) {
+                               warnx("%s: ASPA: invalid ProviderASSet order",
+                                   p->fn);
+                               return 0;
+                       }
+
+               for (j = 0; j < i; j++) {
+                       if (p->res->providers[j].as == aspa_p.as) {
+                               warnx("%s: ASPA: duplicate ProviderAS", p->fn);
+                               return 0;
+                       }
+               }
+
+               if (pa->afiLimit == NULL)
+                       aspa_p.afi = AFI_BOTH;
+               else if (!ip_addr_afi_parse(p->fn, pa->afiLimit, &aspa_p.afi)) {
+                       warnx("%s: ASPA: invalid afiLimit", p->fn);
+                       return 0;
+               }
+
+               p->res->providers[p->res->providersz++] = aspa_p;
+       }
+
+       return 1;
+}
+
+/*
+ * Parse the eContent of an ASPA file.
+ * Returns zero on failure, non-zero on success.
+ */
+static int
+aspa_parse_econtent(const unsigned char *d, size_t dsz, struct parse *p)
+{
+       ASProviderAttestation   *aspa;
+       int                      rc = 0;
+
+       if ((aspa = d2i_ASProviderAttestation(NULL, &d, dsz)) == NULL) {
+               cryptowarnx("%s: ASPA: failed to parse ASProviderAttestation",
+                   p->fn);
+               goto out;
+       }
+
+       if (!valid_econtent_version(p->fn, aspa->version))
+               goto out;
+
+       if (aspa->customerASID == NULL) {
+               warnx("%s: ASPA: customerASID must be present", p->fn);
+               goto out;
+       }
+
+       if (!as_id_parse(aspa->customerASID, &p->res->custasid)) {
+               warnx("%s: malformed CustomerASID", p->fn);
+               goto out;
+       }
+
+       if (!aspa_parse_providers(p, aspa->providers))
+               goto out;
+
+       rc = 1;
+ out:
+       ASProviderAttestation_free(aspa);
+       return rc;
+}
+
+/*
+ * Parse a full ASPA file.
+ * Returns the payload or NULL if the file was malformed.
+ */
+struct aspa *
+aspa_parse(X509 **x509, const char *fn, const unsigned char *der, size_t len)
+{
+       struct parse     p;
+       size_t           cmsz;
+       unsigned char   *cms;
+       const ASN1_TIME *at;
+       int              rc = 0;
+
+       memset(&p, 0, sizeof(struct parse));
+       p.fn = fn;
+
+       cms = cms_parse_validate(x509, fn, der, len, aspa_oid, &cmsz);
+       if (cms == NULL)
+               return NULL;
+
+       if ((p.res = calloc(1, sizeof(*p.res))) == NULL)
+               err(1, NULL);
+
+       if (!x509_get_aia(*x509, fn, &p.res->aia))
+               goto out;
+       if (!x509_get_aki(*x509, fn, &p.res->aki))
+               goto out;
+       if (!x509_get_ski(*x509, fn, &p.res->ski))
+               goto out;
+       if (p.res->aia == NULL || p.res->aki == NULL || p.res->ski == NULL) {
+               warnx("%s: RFC 6487 section 4.8: "
+                   "missing AIA, AKI or SKI X509 extension", fn);
+               goto out;
+       }
+
+       if (X509_get_ext_by_NID(*x509, NID_sbgp_ipAddrBlock, -1) != -1) {
+               warnx("%s: superfluous IP Resources extension present", fn);
+               goto out;
+       }
+
+       at = X509_get0_notAfter(*x509);
+       if (at == NULL) {
+               warnx("%s: X509_get0_notAfter failed", fn);
+               goto out;
+       }
+       if (x509_get_time(at, &p.res->expires) == -1) {
+               warnx("%s: ASN1_time_parse failed", fn);
+               goto out;
+       }
+
+       if (!aspa_parse_econtent(cms, cmsz, &p))
+               goto out;
+
+       rc = 1;
+ out:
+       if (rc == 0) {
+               aspa_free(p.res);
+               p.res = NULL;
+               X509_free(*x509);
+               *x509 = NULL;
+       }
+       free(cms);
+       return p.res;
+}
+
+/*
+ * Free a ASPA pointer.
+ * Safe to call with NULL.
+ */
+void
+aspa_free(struct aspa *p)
+{
+       if (p == NULL)
+               return;
+
+       free(p->aia);
+       free(p->aki);
+       free(p->ski);
+       free(p->providers);
+       free(p);
+}
+
+/*
+ * Serialise parsed ASPA content.
+ * See aspa_read() for the reader on the other side.
+ */
+void
+aspa_buffer(struct ibuf *b, const struct aspa *p)
+{
+       io_simple_buffer(b, &p->valid, sizeof(p->valid));
+       io_simple_buffer(b, &p->custasid, sizeof(p->custasid));
+       io_simple_buffer(b, &p->expires, sizeof(p->expires));
+
+       io_simple_buffer(b, &p->providersz, sizeof(size_t));
+       io_simple_buffer(b, p->providers,
+           p->providersz * sizeof(p->providers[0]));
+
+       io_str_buffer(b, p->aia);
+       io_str_buffer(b, p->aki);
+       io_str_buffer(b, p->ski);
+}
+
+/*
+ * Read parsed ASPA content from descriptor.
+ * See aspa_buffer() for writer.
+ * Result must be passed to aspa_free().
+ */
+struct aspa *
+aspa_read(struct ibuf *b)
+{
+       struct aspa     *p;
+
+       if ((p = calloc(1, sizeof(struct aspa))) == NULL)
+               err(1, NULL);
+
+       io_read_buf(b, &p->valid, sizeof(p->valid));
+       io_read_buf(b, &p->custasid, sizeof(p->custasid));
+       io_read_buf(b, &p->expires, sizeof(p->expires));
+
+       io_read_buf(b, &p->providersz, sizeof(size_t));
+       if ((p->providers = calloc(p->providersz,
+           sizeof(struct aspa_provider))) == NULL)
+               err(1, NULL);
+       io_read_buf(b, p->providers, p->providersz * sizeof(p->providers[0]));
+
+       io_read_str(b, &p->aia);
+       io_read_str(b, &p->aki);
+       io_read_str(b, &p->ski);
+       assert(p->aia && p->aki && p->ski);
+
+       return p;
+}
+
+/*
+ * draft-ietf-sidrops-8210bis § 5.12 states:
+ *
+ *     "The router MUST see at most one ASPA for a given AFI from a cache for
+ *      a particular Customer ASID active at any time. As a number of 
conditions
+ *      in the global RPKI may present multiple valid ASPA RPKI records for a
+ *      single customer to a particular RP cache, this places a burden on the
+ *      cache to form the union of multiple ASPA records it has received from
+ *      the global RPKI into one RPKI-To-Router (RTR) ASPA PDU."
+ *
+ * The above described 'burden' (which is specific to RTR) is resolved in
+ * insert_vap() and aspa_insert_vaps() functions below.
+ *
+ * XXX: for bgpd(8), ASPA config injection (via /var/db/rpki-client/openbgpd)
+ * we probably want to undo the 'burden solving' and compress into implicit
+ * AFIs.
+ */
+
+/*
+ * If the CustomerASID (CAS) showed up before, append the ProviderAS (PAS);
+ * otherwise create a new entry in the RB tree.
+ * Ensure there are no duplicates in the 'providers' array.
+ * Always compare 'expires': use the soonest expiration moment.
+ */
+static int
+insert_vap(struct vap_tree *tree, uint32_t cas, uint32_t pas, time_t expires,
+    enum afi afi)
+{
+       struct vap      *v, *found;
+       int              append;
+       size_t           i;
+
+       if ((v = malloc(sizeof(*v))) == NULL)
+               err(1, NULL);
+       v->afi = afi;
+       v->custasid = cas;
+       v->expires = expires;
+
+       if ((found = RB_INSERT(vap_tree, tree, v)) != NULL) {
+               if (found->expires > expires)
+                       found->expires = expires;
+
+               append = 1;
+               for (i = 0; i < found->providersz; i++)
+                       if (found->providers[i] == pas) {
+                               append = 0;
+                               break;
+                       }
+               if (append) {
+                       found->providers = reallocarray(found->providers,
+                           found->providersz + 1, sizeof(uint32_t));
+                       if (found->providers == NULL)
+                               err(1, NULL);
+                       found->providers[found->providersz++] = pas;
+               }
+
+               free(v);
+       } else {
+               if ((v->providers = malloc(sizeof(uint32_t))) == NULL)
+                       err(1, NULL);
+               v->providers[0] = pas;
+               v->providersz = 1;
+       }
+
+       return 1;
+}
+
+/*
+ * Add each ProviderAS entry into the Validated ASPA Providers (VAP) tree.
+ * Updates "vaps" to be the total number of VAPs, and "uniqs" to be the
+ * pre-'AFI explosion' deduplicated count.
+ */
+void
+aspa_insert_vaps(struct vap_tree *tree, struct aspa *aspa, size_t *vaps,
+    size_t *uniqs)
+{
+       size_t           i;
+       uint32_t         cas, pas;
+       time_t           expires;
+
+       cas = aspa->custasid;
+       expires = aspa->expires;
+
+       for (i = 0; i < aspa->providersz; i++) {
+               pas = aspa->providers[i].as;
+
+               switch (aspa->providers[i].afi) {
+               case AFI_IPV4:
+                       if (!insert_vap(tree, cas, pas, expires, AFI_IPV4))
+                               err(1, NULL);
+                       (*vaps)++;
+                       break;
+               case AFI_IPV6:
+                       if (!insert_vap(tree, cas, pas, expires, AFI_IPV6))
+                               err(1, NULL);
+                       (*vaps)++;
+                       break;
+               case AFI_BOTH:
+                       if (!insert_vap(tree, cas, pas, expires, AFI_IPV4))
+                               err(1, NULL);
+                       (*vaps)++;
+                       if (!insert_vap(tree, cas, pas, expires, AFI_IPV6))
+                               err(1, NULL);
+                       (*vaps)++;
+                       break;
+               }
+               (*uniqs)++;
+       }
+}
+
+static inline int
+vapcmp(struct vap *a, struct vap *b)
+{
+       if (a->afi > b->afi)
+               return 1;
+       if (a->afi < b->afi)
+               return -1;
+
+       if (a->custasid > b->custasid)
+               return 1;
+       if (a->custasid < b->custasid)
+               return -1;
+
+       return 0;
+}
+
+RB_GENERATE(vap_tree, vap, entry, vapcmp);
Index: extern.h
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/extern.h,v
retrieving revision 1.147
diff -u -p -r1.147 extern.h
--- extern.h    10 Aug 2022 10:27:03 -0000      1.147
+++ extern.h    13 Aug 2022 00:24:24 -0000
@@ -74,7 +74,8 @@ struct cert_as {
  */
 enum afi {
        AFI_IPV4 = 1,
-       AFI_IPV6 = 2
+       AFI_IPV6 = 2,
+       AFI_BOTH = 3
 };
 
 /*
@@ -283,6 +284,45 @@ struct gbr {
        char            *ski; /* SKI */
 };
 
+struct aspa_provider {
+       uint32_t         as;
+       enum afi         afi;
+};
+
+/*
+ * A single ASPA record
+ */
+struct aspa {
+       int                      valid; /* contained in parent auth */
+       int                      talid; /* TAL the ASPA is chained up to */
+       char                    *aia; /* AIA */
+       char                    *aki; /* AKI */
+       char                    *ski; /* SKI */
+       uint32_t                 custasid; /* the customerASID */
+       struct aspa_provider    *providers; /* the providers */
+       size_t                   providersz; /* number of providers */
+       time_t                   expires; /* NotAfter of the ASPA EE cert */
+};
+
+/*
+ * A Validated ASPA Payload (VAP) tree element.
+ * To ease transformation, this struct mimicks ASPA RTR PDU structure.
+ */
+struct vap {
+       RB_ENTRY(vap)            entry;
+       enum afi                 afi;
+       uint32_t                 custasid;
+       uint32_t                *providers;
+       size_t                   providersz;
+       time_t                   expires;
+};
+
+/*
+ * Tree of VAPs sorted by afi, custasid, and provideras.
+ */
+RB_HEAD(vap_tree, vap);
+RB_PROTOTYPE(vap_tree, vap, entry, vapcmp);
+
 /*
  * A single VRP element (including ASID)
  */
@@ -433,6 +473,11 @@ struct stats {
        size_t   rrdp_fails; /* failed rrdp repositories */
        size_t   crls; /* revocation lists */
        size_t   gbrs; /* ghostbuster records */
+       size_t   aspas; /* ASPA objects */
+       size_t   aspas_fail; /* ASPA objects failing syntactic parse */
+       size_t   aspas_invalid; /* ASPAs with invalid customerASID */
+       size_t   vaps; /* total number of Validated ASPA Payloads */
+       size_t   vaps_uniqs; /* total number of unique VAPs */
        size_t   vrps; /* total number of vrps */
        size_t   uniqs; /* number of unique vrps */
        size_t   del_files; /* number of files removed in cleanup */
@@ -496,6 +541,14 @@ void                rsc_free(struct rsc *);
 struct rsc     *rsc_parse(X509 **, const char *, const unsigned char *,
                    size_t);
 
+void            aspa_buffer(struct ibuf *, const struct aspa *);
+void            aspa_free(struct aspa *);
+void            aspa_insert_vaps(struct vap_tree *, struct aspa *, size_t *,
+                   size_t *);
+struct aspa    *aspa_parse(X509 **, const char *, const unsigned char *,
+                   size_t);
+struct aspa    *aspa_read(struct ibuf *);
+
 /* crl.c */
 struct crl     *crl_parse(const char *, const unsigned char *, size_t);
 struct crl     *crl_get(struct crl_tree *, const struct auth *);
@@ -518,6 +571,7 @@ int          valid_origin(const char *, const c
 int             valid_x509(char *, X509_STORE_CTX *, X509 *, struct auth *,
                    struct crl *, int);
 int             valid_rsc(const char *, struct auth *, struct rsc *);
+int             valid_aspa(const char *, struct auth *, struct aspa *);
 int             valid_econtent_version(const char *, const ASN1_INTEGER *);
 
 /* Working with CMS. */
@@ -664,6 +718,7 @@ void                 mft_print(const X509 *, const str
 void            roa_print(const X509 *, const struct roa *);
 void            gbr_print(const X509 *, const struct gbr *);
 void            rsc_print(const X509 *, const struct rsc *);
+void            aspa_print(const X509 *, const struct aspa *);
 
 /* Output! */
 
@@ -674,20 +729,20 @@ extern int         outformats;
 #define FORMAT_JSON    0x08
 
 int             outputfiles(struct vrp_tree *v, struct brk_tree *b,
-                   struct stats *);
+                   struct vap_tree *, struct stats *);
 int             outputheader(FILE *, struct stats *);
 int             output_bgpd(FILE *, struct vrp_tree *, struct brk_tree *,
-                   struct stats *);
+                   struct vap_tree *, struct stats *);
 int             output_bird1v4(FILE *, struct vrp_tree *, struct brk_tree *,
-                   struct stats *);
+                   struct vap_tree *, struct stats *);
 int             output_bird1v6(FILE *, struct vrp_tree *, struct brk_tree *,
-                   struct stats *);
+                   struct vap_tree *, struct stats *);
 int             output_bird2(FILE *, struct vrp_tree *, struct brk_tree *,
-                   struct stats *);
+                   struct vap_tree *, struct stats *);
 int             output_csv(FILE *, struct vrp_tree *, struct brk_tree *,
-                   struct stats *);
+                   struct vap_tree *, struct stats *);
 int             output_json(FILE *, struct vrp_tree *, struct brk_tree *,
-                   struct stats *);
+                   struct vap_tree *, struct stats *);
 
 void           logx(const char *fmt, ...)
                    __attribute__((format(printf, 1, 2)));
@@ -719,6 +774,9 @@ int mkpathat(int, const char *);
 
 /* Maximum number of FileAndHash entries per manifest. */
 #define MAX_MANIFEST_ENTRIES   100000
+
+/* Maximum number of Providers per ASPA object. */
+#define MAX_ASPA_PROVIDERS     10000
 
 /* Maximum depth of the RPKI tree. */
 #define MAX_CERT_DEPTH         12
Index: filemode.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/filemode.c,v
retrieving revision 1.7
diff -u -p -r1.7 filemode.c
--- filemode.c  11 May 2022 14:42:01 -0000      1.7
+++ filemode.c  13 Aug 2022 00:24:24 -0000
@@ -265,6 +265,7 @@ proc_parser_file(char *file, unsigned ch
        struct gbr *gbr = NULL;
        struct tal *tal = NULL;
        struct rsc *rsc = NULL;
+       struct aspa *aspa = NULL;
        char *aia = NULL, *aki = NULL;
        char filehash[SHA256_DIGEST_LENGTH];
        char *hash;
@@ -366,6 +367,14 @@ proc_parser_file(char *file, unsigned ch
                aia = rsc->aia;
                aki = rsc->aki;
                break;
+       case RTYPE_ASPA:
+               aspa = aspa_parse(&x509, file, buf, len);
+               if (aspa == NULL)
+                       break;
+               aspa_print(x509, aspa);
+               aia = aspa->aia;
+               aki = aspa->aki;
+               break;
        default:
                printf("%s: unsupported file type\n", file);
                break;
@@ -391,10 +400,19 @@ proc_parser_file(char *file, unsigned ch
                c = crl_get(&crlt, a);
 
                if ((status = valid_x509(file, ctx, x509, a, c, 0))) {
-                       if (type == RTYPE_ROA)
+                       switch (type) {
+                       case RTYPE_ROA:
                                status = valid_roa(file, a, roa);
-                       else if (type == RTYPE_RSC)
+                               break;
+                       case RTYPE_RSC:
                                status = valid_rsc(file, a, rsc);
+                               break;
+                       case RTYPE_ASPA:
+                               status = valid_aspa(file, a, aspa);
+                               break;
+                       default:
+                               break;
+                       }
                }
                if (status)
                        printf("OK");
@@ -431,6 +449,7 @@ proc_parser_file(char *file, unsigned ch
        roa_free(roa);
        gbr_free(gbr);
        tal_free(tal);
+       aspa_free(aspa);
 }
 
 /*
Index: main.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/main.c,v
retrieving revision 1.209
diff -u -p -r1.209 main.c
--- main.c      4 Aug 2022 13:44:07 -0000       1.209
+++ main.c      13 Aug 2022 00:24:25 -0000
@@ -1,4 +1,4 @@
-/*     $OpenBSD: main.c,v 1.209 2022/08/04 13:44:07 claudio Exp $ */
+/*     $OpenBSD: main.c,v 1.208 2022/06/27 10:18:27 job Exp $ */
 /*
  * Copyright (c) 2021 Claudio Jeker <clau...@openbsd.org>
  * Copyright (c) 2019 Kristaps Dzonsons <krist...@bsd.lv>
@@ -475,13 +475,14 @@ queue_add_from_cert(const struct cert *c
  */
 static void
 entity_process(struct ibuf *b, struct stats *st, struct vrp_tree *tree,
-    struct brk_tree *brktree)
+    struct brk_tree *brktree, struct vap_tree *vaptree)
 {
        enum rtype       type;
        struct tal      *tal;
        struct cert     *cert;
        struct mft      *mft;
        struct roa      *roa;
+       struct aspa     *aspa;
        char            *file;
        int              c;
 
@@ -568,6 +569,21 @@ entity_process(struct ibuf *b, struct st
                break;
        case RTYPE_FILE:
                break;
+       case RTYPE_ASPA:
+               st->aspas++;
+               io_read_buf(b, &c, sizeof(c));
+               if (c == 0) {
+                       st->aspas_fail++;
+                       break;
+               }
+               aspa = aspa_read(b);
+               if (aspa->valid)
+                       aspa_insert_vaps(vaptree, aspa, &st->vaps,
+                           &st->vaps_uniqs);
+               else
+                       st->aspas_invalid++;
+               aspa_free(aspa);
+               break;
        default:
                errx(1, "unknown entity type %d", type);
        }
@@ -793,6 +809,7 @@ main(int argc, char *argv[])
        const char      *skiplistfile = NULL;
        struct vrp_tree  vrps = RB_INITIALIZER(&vrps);
        struct brk_tree  brks = RB_INITIALIZER(&brks);
+       struct vap_tree  vaps = RB_INITIALIZER(&vaps);
        struct rusage    ru;
        struct timeval   start_time, now_time;
 
@@ -1006,7 +1023,8 @@ main(int argc, char *argv[])
                signal(SIGALRM, suicide);
        }
 
-       if (pledge("stdio rpath wpath cpath fattr sendfd unveil", NULL) == -1)
+       /* TODO unveil cachedir and outputdir, no other access allowed */
+       if (pledge("stdio rpath wpath cpath fattr sendfd", NULL) == -1)
                err(1, "pledge");
 
        msgbuf_init(&procq);
@@ -1047,17 +1065,7 @@ main(int argc, char *argv[])
        if (filemode) {
                while (*argv != NULL)
                        queue_add_file(*argv++, RTYPE_FILE, 0);
-
-               if (unveil(cachedir, "r") == -1)
-                       err(1, "unveil cachedir");
-       } else {
-               if (unveil(outputdir, "rwc") == -1)
-                       err(1, "unveil outputdir");
-               if (unveil(cachedir, "rwc") == -1)
-                       err(1, "unveil cachedir");
        }
-       if (pledge("stdio rpath wpath cpath fattr sendfd", NULL) == -1)
-               err(1, "unveil");
 
        /* change working directory to the cache directory */
        if (fchdir(cachefd) == -1)
@@ -1159,7 +1167,7 @@ main(int argc, char *argv[])
                if ((pfd[0].revents & POLLIN)) {
                        b = io_buf_read(proc, &procbuf);
                        if (b != NULL) {
-                               entity_process(b, &stats, &vrps, &brks);
+                               entity_process(b, &stats, &vrps, &brks, &vaps);
                                ibuf_free(b);
                        }
                }
@@ -1242,7 +1250,7 @@ main(int argc, char *argv[])
        if (fchdir(outdirfd) == -1)
                err(1, "fchdir output dir");
 
-       if (outputfiles(&vrps, &brks, &stats))
+       if (outputfiles(&vrps, &brks, &vaps, &stats))
                rc = 1;
 
        printf("Processing time %lld seconds "
@@ -1253,6 +1261,8 @@ main(int argc, char *argv[])
        printf("Skiplist entries: %zu\n", stats.skiplistentries);
        printf("Route Origin Authorizations: %zu (%zu failed parse, %zu 
invalid)\n",
            stats.roas, stats.roas_fail, stats.roas_invalid);
+       printf("AS Provider Attestations: %zu (%zu failed parse, %zu 
invalid)\n",
+           stats.aspas, stats.aspas_fail, stats.aspas_invalid);
        printf("BGPsec Router Certificates: %zu\n", stats.brks);
        printf("Certificates: %zu (%zu invalid)\n",
            stats.certs, stats.certs_fail);
@@ -1266,6 +1276,7 @@ main(int argc, char *argv[])
        printf("Cleanup: removed %zu files, %zu directories, %zu superfluous\n",
            stats.del_files, stats.del_dirs, stats.extra_files);
        printf("VRP Entries: %zu (%zu unique)\n", stats.vrps, stats.uniqs);
+       printf("VAP Entries: %zu (%zu unique)\n", stats.vaps, stats.vaps_uniqs);
 
        /* Memory cleanup. */
        repo_free();
Index: output-bgpd.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-bgpd.c,v
retrieving revision 1.23
diff -u -p -r1.23 output-bgpd.c
--- output-bgpd.c       11 Oct 2021 16:50:03 -0000      1.23
+++ output-bgpd.c       13 Aug 2022 00:24:25 -0000
@@ -21,7 +21,7 @@
 
 int
 output_bgpd(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks,
-    struct stats *st)
+    struct vap_tree *vaps, struct stats *st)
 {
        struct vrp      *v;
 
Index: output-bird.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-bird.c,v
retrieving revision 1.14
diff -u -p -r1.14 output-bird.c
--- output-bird.c       15 May 2022 16:43:34 -0000      1.14
+++ output-bird.c       13 Aug 2022 00:24:25 -0000
@@ -22,7 +22,7 @@
 
 int
 output_bird1v4(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks,
-    struct stats *st)
+    struct vap_tree *vaps, struct stats *st)
 {
        extern          const char *bird_tablename;
        struct vrp      *v;
@@ -51,7 +51,7 @@ output_bird1v4(FILE *out, struct vrp_tre
 
 int
 output_bird1v6(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks,
-    struct stats *st)
+    struct vap_tree *vaps, struct stats *st)
 {
        extern          const char *bird_tablename;
        struct vrp      *v;
@@ -80,7 +80,7 @@ output_bird1v6(FILE *out, struct vrp_tre
 
 int
 output_bird2(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks,
-    struct stats *st)
+    struct vap_tree *vaps, struct stats *st)
 {
        extern          const char *bird_tablename;
        struct vrp      *v;
Index: output-csv.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-csv.c,v
retrieving revision 1.12
diff -u -p -r1.12 output-csv.c
--- output-csv.c        4 Nov 2021 11:32:55 -0000       1.12
+++ output-csv.c        13 Aug 2022 00:24:25 -0000
@@ -21,7 +21,7 @@
 
 int
 output_csv(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks,
-    struct stats *st)
+    struct vap_tree *vaps, struct stats *st)
 {
        struct vrp      *v;
 
Index: output-json.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-json.c,v
retrieving revision 1.26
diff -u -p -r1.26 output-json.c
--- output-json.c       15 May 2022 16:43:34 -0000      1.26
+++ output-json.c       13 Aug 2022 00:24:25 -0000
@@ -46,6 +46,9 @@ outputheader_json(FILE *out, struct stat
            "\t\t\"roas\": %zu,\n"
            "\t\t\"failedroas\": %zu,\n"
            "\t\t\"invalidroas\": %zu,\n"
+           "\t\t\"aspas\": %zu,\n"
+           "\t\t\"failedaspas\": %zu,\n"
+           "\t\t\"invalidaspas\": %zu,\n"
            "\t\t\"bgpsec_pubkeys\": %zu,\n"
            "\t\t\"certificates\": %zu,\n"
            "\t\t\"invalidcertificates\": %zu,\n"
@@ -55,6 +58,7 @@ outputheader_json(FILE *out, struct stat
            hn, tbuf, (long long)st->elapsed_time.tv_sec,
            (long long)st->user_time.tv_sec, (long long)st->system_time.tv_sec,
            st->roas, st->roas_fail, st->roas_invalid,
+           st->aspas, st->aspas_fail, st->aspas_invalid,
            st->brks, st->certs, st->certs_fail,
            st->tals, talsz - st->tals) < 0)
                return -1;
@@ -76,6 +80,8 @@ outputheader_json(FILE *out, struct stat
            "\t\t\"repositories\": %zu,\n"
            "\t\t\"vrps\": %zu,\n"
            "\t\t\"uniquevrps\": %zu,\n"
+           "\t\t\"vaps\": %zu,\n"
+           "\t\t\"uniquevaps\": %zu,\n"
            "\t\t\"cachedir_del_files\": %zu,\n"
            "\t\t\"cachedir_superfluous_files\": %zu,\n"
            "\t\t\"cachedir_del_dirs\": %zu\n"
@@ -85,14 +91,80 @@ outputheader_json(FILE *out, struct stat
            st->gbrs,
            st->repos,
            st->vrps, st->uniqs,
+           st->vaps, st->vaps_uniqs,
            st->del_files, st->extra_files, st->del_dirs) < 0)
                return -1;
        return 0;
 }
 
+static int
+print_vap(FILE *out, struct vap *v)
+{
+       size_t i;
+
+       if (fprintf(out, "\t\t\t{ \"custasid\": %u, \"providers\": [",
+           v->custasid) < 0)
+               return -1;
+       for (i = 0; i < v->providersz; i++) {
+               if (fprintf(out, "%u", v->providers[i]) < 0)
+                       return -1;
+               if (i + 1 < v->providersz)
+                       if (fprintf(out, ", ") < 0)
+                               return -1;
+       }
+       if (fprintf(out, "], \"expires\": %lld }", (long long)v->expires) < 0)
+               return -1;
+
+       return 0;
+}
+
+static int
+output_aspa(FILE *out, struct vap_tree *vaps)
+{
+       struct vap      *v;
+       int              first;
+
+       if (fprintf(out, "\n\t],\n\n\t\"provider_authorizations\": {\n"
+           "\t\t\"ipv4\": [\n") < 0)
+               return -1;
+
+       first = 1;
+       RB_FOREACH(v, vap_tree, vaps)
+               if (v->afi == AFI_IPV4) {
+                       if (!first) {
+                               if (fprintf(out, ",\n") < 0)
+                                       return -1;
+                       }
+                       first = 0;
+                       if (print_vap(out, v))
+                               return -1;
+               }
+
+       if (fprintf(out, "\n\t\t],\n\t\t\"ipv6\": [\n") < 0)
+               return -1;
+
+       first = 1;
+       RB_FOREACH(v, vap_tree, vaps)
+               if (v->afi == AFI_IPV6) {
+                       if (!first) {
+                               if (fprintf(out, ",\n") < 0)
+                                       return -1;
+                       }
+                       first = 0;
+                               if (print_vap(out, v))
+                                       return -1;
+               }
+
+       if (fprintf(out, "\n\t\t]\n\t}\n") < 0)
+               return -1;
+       
+       return 0;
+}
+
+
 int
 output_json(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks,
-    struct stats *st)
+    struct vap_tree *vaps, struct stats *st)
 {
        char             buf[64];
        struct vrp      *v;
@@ -140,7 +212,11 @@ output_json(FILE *out, struct vrp_tree *
                        return -1;
        }
 
-       if (fprintf(out, "\n\t]\n}\n") < 0)
+       if (output_aspa(out, vaps) < 0)
                return -1;
+
+       if (fprintf(out, "\n}\n") < 0)
+                return -1;
+
        return 0;
 }
Index: output.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output.c,v
retrieving revision 1.26
diff -u -p -r1.26 output.c
--- output.c    20 Apr 2022 15:29:24 -0000      1.26
+++ output.c    13 Aug 2022 00:24:25 -0000
@@ -65,7 +65,7 @@ static const struct outputs {
        int      format;
        char    *name;
        int     (*fn)(FILE *, struct vrp_tree *, struct brk_tree *,
-                   struct stats *);
+                   struct vap_tree *, struct stats *);
 } outputs[] = {
        { FORMAT_OPENBGPD, "openbgpd", output_bgpd },
        { FORMAT_BIRD, "bird1v4", output_bird1v4 },
@@ -83,7 +83,8 @@ static void    sig_handler(int);
 static void     set_signal_handler(void);
 
 int
-outputfiles(struct vrp_tree *v, struct brk_tree *b, struct stats *st)
+outputfiles(struct vrp_tree *v, struct brk_tree *b, struct vap_tree *a,
+    struct stats *st)
 {
        int i, rc = 0;
 
@@ -102,7 +103,7 @@ outputfiles(struct vrp_tree *v, struct b
                        rc = 1;
                        continue;
                }
-               if ((*outputs[i].fn)(fout, v, b, st) != 0) {
+               if ((*outputs[i].fn)(fout, v, b, a, st) != 0) {
                        warn("output for %s format failed", outputs[i].name);
                        fclose(fout);
                        output_cleantmp();
Index: parser.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/parser.c,v
retrieving revision 1.73
diff -u -p -r1.73 parser.c
--- parser.c    21 Apr 2022 12:59:03 -0000      1.73
+++ parser.c    13 Aug 2022 00:24:25 -0000
@@ -492,6 +492,46 @@ proc_parser_gbr(char *file, const unsign
 }
 
 /*
+ * Parse an ASPA object
+ */
+static struct aspa *
+proc_parser_aspa(char *file, const unsigned char *der, size_t len)
+{
+       struct aspa             *aspa;
+       struct auth             *a;
+       struct crl              *crl;
+       X509                    *x509;
+
+       if ((aspa = aspa_parse(&x509, file, der, len)) == NULL)
+               return NULL;
+
+       a = valid_ski_aki(file, &auths, aspa->ski, aspa->aki);
+       crl = crl_get(&crlt, a);
+
+       if (!valid_x509(file, ctx, x509, a, crl, 0)) {
+               X509_free(x509);
+               aspa_free(aspa);
+               return NULL;
+       }
+       X509_free(x509);
+
+       aspa->talid = a->cert->talid;
+
+       if (valid_aspa(file, a, aspa))
+               aspa->valid = 1;
+
+       if (crl != NULL && aspa->expires > crl->expires)
+               aspa->expires = crl->expires;
+
+       for (; a != NULL; a = a->parent) {
+               if (aspa->expires > a->cert->expires)
+                       aspa->expires = a->cert->expires;
+       }
+
+       return aspa;
+}
+
+/*
  * Load the file specified by the entity information.
  */
 static char *
@@ -522,6 +562,7 @@ parse_entity(struct entityq *q, struct m
        struct cert     *cert;
        struct mft      *mft;
        struct roa      *roa;
+       struct aspa     *aspa;
        struct ibuf     *b;
        unsigned char   *f;
        size_t           flen;
@@ -606,6 +647,16 @@ parse_entity(struct entityq *q, struct m
                        file = parse_load_file(entp, &f, &flen);
                        io_str_buffer(b, file);
                        proc_parser_gbr(file, f, flen);
+                       break;
+               case RTYPE_ASPA:
+                       file = parse_load_file(entp, &f, &flen);
+                       io_str_buffer(b, file);
+                       aspa = proc_parser_aspa(file, f, flen);
+                       c = (aspa != NULL);
+                       io_simple_buffer(b, &c, sizeof(int));
+                       if (aspa != NULL)
+                               aspa_buffer(b, aspa);
+                       aspa_free(aspa);
                        break;
                default:
                        errx(1, "unhandled entity type %d", entp->type);
Index: print.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/print.c,v
retrieving revision 1.14
diff -u -p -r1.14 print.c
--- print.c     14 Jul 2022 13:24:56 -0000      1.14
+++ print.c     13 Aug 2022 00:24:25 -0000
@@ -568,3 +568,52 @@ rsc_print(const X509 *x, const struct rs
        if (outformats & FORMAT_JSON)
                printf("\t],\n");
 }
+
+void
+aspa_print(const X509 *x, const struct aspa *p)
+{
+       size_t  i;
+
+       if (outformats & FORMAT_JSON) {
+               printf("\t\"type\": \"aspa\",\n");
+               printf("\t\"ski\": \"%s\",\n", pretty_key_id(p->ski));
+               x509_print(x);
+               printf("\t\"aki\": \"%s\",\n", pretty_key_id(p->aki));
+               printf("\t\"aia\": \"%s\",\n", p->aia);
+               printf("\t\"customer_asid\": %u,\n", p->custasid);
+               printf("\t\"provider_set\": [\n");
+               for (i = 0; i < p->providersz; i++) {
+                       printf("\t\t{ \"asid\": %u", p->providers[i].as);
+                       if (p->providers[i].afi == AFI_IPV4)
+                               printf(", \"afi_limit\": \"ipv4\"");
+                       if (p->providers[i].afi == AFI_IPV6)
+                               printf(", \"afi_limit\": \"ipv6\"");
+                       printf(" }");
+                       if (i + 1 < p->providersz)
+                               printf(",");
+                       printf("\n");   
+               }
+               printf("\t],\n");
+       } else {
+               printf("Subject key identifier: %s\n", pretty_key_id(p->ski));
+               x509_print(x);
+               printf("Authority key identifier: %s\n", pretty_key_id(p->aki));
+               printf("Authority info access: %s\n", p->aia);
+               printf("Customer AS: %u\n", p->custasid);
+               printf("Provider Set:\n");
+               for (i = 0; i < p->providersz; i++) {
+                       printf("%5zu: AS: %d", i + 1, p->providers[i].as);
+                       switch (p->providers[i].afi) {
+                       case AFI_IPV4:
+                               printf(" (IPv4 only)");
+                               break;
+                       case AFI_IPV6:
+                               printf(" (IPv6 only)");
+                               break;
+                       default:
+                               break;
+                       }
+                       printf("\n");
+               }
+       }
+}
Index: roa.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/roa.c,v
retrieving revision 1.49
diff -u -p -r1.49 roa.c
--- roa.c       10 Aug 2022 14:54:03 -0000      1.49
+++ roa.c       13 Aug 2022 00:24:25 -0000
@@ -389,6 +389,8 @@ vrpcmp(struct vrp *a, struct vrp *b)
                if (rv)
                        return rv;
                break;
+       default:
+               break;
        }
        /* a smaller prefixlen is considered bigger, e.g. /8 vs /10 */
        if (a->addr.prefixlen < b->addr.prefixlen)
Index: rpki-client.8
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/rpki-client.8,v
retrieving revision 1.68
diff -u -p -r1.68 rpki-client.8
--- rpki-client.8       30 Jun 2022 10:27:52 -0000      1.68
+++ rpki-client.8       13 Aug 2022 00:24:25 -0000
@@ -295,6 +295,8 @@ Certification Requests.
 Resource Public Key Infrastructure (RPKI) Trust Anchor Locator.
 .It draft-ietf-sidrops-rpki-rsc-08
 A profile for Resource Public Key Infrastructure (RPKI) Signed Checklists 
(RSC).
+.It draft-ietf-sidrops-aspa-profile-10
+A Profile for Autonomous System Provider Authorization (ASPA).
 .El
 .Sh HISTORY
 .Nm
Index: validate.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/validate.c,v
retrieving revision 1.40
diff -u -p -r1.40 validate.c
--- validate.c  10 Jun 2022 10:36:43 -0000      1.40
+++ validate.c  13 Aug 2022 00:24:25 -0000
@@ -533,3 +533,20 @@ valid_econtent_version(const char *fn, c
                return 0;
        }
 }
+
+/*
+ * Validate the ASPA: check that the customerASID is contained.
+ * Returns 1 if valid, 0 otherwise.
+ */
+int
+valid_aspa(const char *fn, struct auth *a, struct aspa *aspa)
+{
+
+       if (valid_as(a, aspa->custasid, aspa->custasid))
+               return 1;
+       else
+               warnx("%s: ASPA: uncovered Customer ASID: %u", fn,
+                   aspa->custasid);
+
+       return 0;
+}
Index: x509.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/x509.c,v
retrieving revision 1.47
diff -u -p -r1.47 x509.c
--- x509.c      28 Jul 2022 16:03:19 -0000      1.47
+++ x509.c      13 Aug 2022 00:24:25 -0000
@@ -44,6 +44,7 @@ ASN1_OBJECT   *msg_dgst_oid;  /* pkcs-9 id-
 ASN1_OBJECT    *sign_time_oid; /* pkcs-9 id-signingTime */
 ASN1_OBJECT    *bin_sign_time_oid;     /* pkcs-9 id-aa-binarySigningTime */
 ASN1_OBJECT    *rsc_oid;       /* id-ct-signedChecklist */
+ASN1_OBJECT    *aspa_oid;      /* id-ct-ASPA */
 
 void
 x509_init_oid(void)
@@ -81,6 +82,9 @@ x509_init_oid(void)
        if ((rsc_oid = OBJ_txt2obj("1.2.840.113549.1.9.16.1.48", 1)) == NULL)
                errx(1, "OBJ_txt2obj for %s failed",
                    "1.2.840.113549.1.9.16.1.48");
+       if ((aspa_oid = OBJ_txt2obj("1.2.840.113549.1.9.16.1.49", 1)) == NULL)
+               errx(1, "OBJ_txt2obj for %s failed",
+                   "1.2.840.113549.1.9.16.1.49");
 }
 
 /*

Reply via email to