Skip to content

Instantly share code, notes, and snippets.

@MathildeRoussel
Created May 18, 2018 13:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MathildeRoussel/cf204aef51a42f46ae4ccb034be3d966 to your computer and use it in GitHub Desktop.
Save MathildeRoussel/cf204aef51a42f46ae4ccb034be3d966 to your computer and use it in GitHub Desktop.
using System;
using System.Configuration;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace WalletPassGenerator
{
public static class Generator
{
private const string ForegroundColor = "rgb(255,255,255)";
private const string BackgroundColor = "rgb(35,37,51)";
private const string LabelColor = "rgb(112,95,173)";
private const string AppleOid = "1.2.840.113635.100.4.14";
[FunctionName("GeneratePass")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "pass/{eventId}")]HttpRequestMessage req, string eventId, TraceWriter log, ExecutionContext executionContext)
{
try
{
var eventData = FetchEventData(eventId);
var attendee = await req.Content.ReadAsAsync<Attendee>() ?? new Attendee { Email = "toto@toto.com", Name = "Toto Leheros" };
var signatureData = await CreateSignatureDataAsync();
var memoryStream = new MemoryStream();
using (var pkpass = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
{
var passJson = pkpass.CreateEntry("pass.json");
var passJsonHash = await CreatePassJsonAsync(passJson, signatureData, eventData, attendee);
var manifest = new JObject(new JProperty("pass.json", passJsonHash));
async Task AddImageAsync(string path)
{
var hash = await ConvertFileToEntryAsync(pkpass.CreateEntry(path), Path.Combine(executionContext.FunctionAppDirectory, "Assets", path));
manifest.Add(new JProperty(path, hash));
}
await AddImageAsync("strip.png");
await AddImageAsync("strip@2x.png");
await AddImageAsync("icon.png");
await AddImageAsync("icon@2x.png");
await AddImageAsync("logo.png");
await AddImageAsync("logo@2x.png");
var manifestEntry = pkpass.CreateEntry("manifest.json");
var dataToSign = await CreateManifestJsonAsync(manifestEntry, manifest);
await CreateSignatureAsync(pkpass.CreateEntry("signature"), signatureData, dataToSign);
}
memoryStream.Position = 0;
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StreamContent(memoryStream)
{
Headers =
{
ContentType = new MediaTypeHeaderValue("application/vnd.apple.pkpass"),
ContentDisposition = new ContentDispositionHeaderValue("attachment") {FileName = $"pass-{eventId}.pkpass"}
}
}
};
}
catch (Exception ex)
{
log.Error("An error occured", ex);
throw;
}
}
private static async Task<SignatureData> CreateSignatureDataAsync()
{
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
var secret = await keyVaultClient.GetSecretAsync(ConfigurationManager.AppSettings["KeyvaultUri"], ConfigurationManager.AppSettings["PassTypeIdentifier"].Replace(".", "-"));
var collection = new X509Certificate2Collection();
collection.Import(Convert.FromBase64String(secret.Value), null, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
var authorityCertificate = collection.Find(X509FindType.FindBySubjectName, "Apple Worldwide Developer Relations Certification Authority", false)[0];
var signingCertificate = collection.Find(X509FindType.FindBySubjectName, $"Pass Type ID: {ConfigurationManager.AppSettings["PassTypeIdentifier"]}", false)[0];
var teamIdentifier = Regex.Match(signingCertificate.Subject, @"OU=(?<teamId>\w+),").Groups["teamId"].Value;
return new SignatureData
{
SigningCertificate = signingCertificate,
Authority = authorityCertificate,
TeamIdentifier = teamIdentifier,
PassTypeIdentifier = ConfigurationManager.AppSettings["PassTypeIdentifier"]
};
}
private static EventData FetchEventData(string eventId)
{
return new EventData
{
Id = "matinaleserverless052018",
Organizer = "Cellenza",
Type = "Matinale Cellenza",
Link = "http://click.cellenza.com/inscription-matinale-31-mai-architectures-modernes",
DateTime = new DateTimeOffset(2018, 05, 29, 09, 00, 00, TimeSpan.FromHours(2)),
Title = "Matinale Serverless",
SubTitle = "Serverless, microservices et containers",
Speaker = "Marius Zaharia, Michel Hubert",
Location = new EventLocation
{
Address = "City Chateauform, 2 Avenue Vélasquez, 75008 PARIS",
Phone = "+33145631429",
Email = "contact@cellenza.com"
}
};
}
private static async Task<string> CreatePassJsonAsync(ZipArchiveEntry archiveEntry, SignatureData signatureData, EventData eventData, Attendee attendee)
{
var serial = string.Join(":", eventData.Id, GetHashForBytes(Encoding.UTF8.GetBytes(attendee.Email.ToLowerInvariant())));
var passTemplate = new
{
FormatVersion = 1,
PassTypeIdentifier = signatureData.PassTypeIdentifier,
SerialNumber = serial,
TeamIdentifier = signatureData.TeamIdentifier,
OrganizationName = eventData.Organizer,
Description = eventData.Type,
ForegroundColor = ForegroundColor,
BackgroundColor = BackgroundColor,
LabelColor = LabelColor,
Barcode = new
{
Message = serial,
Format = "PKBarcodeFormatQR",
messageEncoding = "iso-8859-1"
},
EventTicket = new
{
HeaderFields = new object[]
{
new
{
Key = "date",
Label = "Date",
IsRelative = true,
Value = eventData.DateTime.ToString("yyyy-MM-ddTHH:mm:sszzz"),
DateStyle = "PKDateStyleShort",
TimeStyle = "PKDateStyleShort",
}
},
PrimaryFields = new object[]
{
new
{
Key = "title",
Label = string.Empty,
Value = eventData.Title
}
},
SecondaryFields = new object[]
{
new
{
Key = "subtitle",
Label = string.Empty,
Value = eventData.SubTitle,
TextAlignment = "PKTextAlignmentLeft"
}
},
AuxiliaryFields = new object[]
{
new
{
Key = "speaker",
Label = "PRESENTE PAR",
Value = eventData.Speaker,
TextAlignment = "PKTextAlignmentLeft"
}
},
BackFields = new object[]
{
new
{
Key = "site",
Label = "PLUS D'INFOS",
Value = eventData.Link
},
new
{
Key = "address",
Label = "ADRESSE",
Value = eventData.Location.Address
},
new
{
Key = "phone",
Label = "TELEPHONE",
Value = eventData.Location.Phone
},
new
{
Key = "email",
Label = "EMAIL",
Value = eventData.Location.Email
}
}
}
};
var serializer = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(), Formatting = Formatting.Indented });
using (var entryStream = archiveEntry.Open())
using (var memoryStream = new MemoryStream())
using (var writer = new StreamWriter(memoryStream) { AutoFlush = true })
{
serializer.Serialize(writer, passTemplate);
memoryStream.Position = 0;
var hash = GetHashForStream(memoryStream);
memoryStream.Position = 0;
await memoryStream.CopyToAsync(entryStream);
return hash;
}
}
private static async Task<string> ConvertFileToEntryAsync(ZipArchiveEntry archiveEntry, string filePath)
{
using (var fileStream = File.OpenRead(filePath))
using (var entryStream = archiveEntry.Open())
{
var hash = GetHashForStream(fileStream);
fileStream.Position = 0;
await fileStream.CopyToAsync(entryStream);
return hash;
}
}
private static async Task<byte[]> CreateManifestJsonAsync(ZipArchiveEntry archiveEntry, JToken manifest)
{
using (var memoryStream = new MemoryStream())
using (var entryStream = archiveEntry.Open())
using (var streamWriter = new StreamWriter(memoryStream) { AutoFlush = true })
{
await streamWriter.WriteAsync(manifest.ToString(Formatting.Indented));
memoryStream.Position = 0;
await memoryStream.CopyToAsync(entryStream);
return memoryStream.ToArray();
}
}
private static string GetHashForBytes(byte[] bytes)
{
using (var sha1 = new SHA1Managed())
{
return string.Concat(sha1.ComputeHash(bytes).Select(x => x.ToString("x2")));
}
}
private static string GetHashForStream(Stream stream)
{
using (var sha1 = new SHA1Managed())
{
return string.Concat(sha1.ComputeHash(stream).Select(x => x.ToString("x2")));
}
}
private static async Task CreateSignatureAsync(ZipArchiveEntry entry, SignatureData signatureData, byte[] dataToSign)
{
var signedCms = new SignedCms(new ContentInfo(new Oid(AppleOid), dataToSign), true);
var cmsSigner = new CmsSigner(SubjectIdentifierType.SubjectKeyIdentifier, signatureData.SigningCertificate) { IncludeOption = X509IncludeOption.None };
cmsSigner.SignedAttributes.Add(new Pkcs9SigningTime());
cmsSigner.Certificates.Add(signatureData.SigningCertificate);
cmsSigner.Certificates.Add(signatureData.Authority);
signedCms.ComputeSignature(cmsSigner);
var bytes = signedCms.Encode();
using (var stream = entry.Open())
{
await stream.WriteAsync(bytes, 0, bytes.Length);
}
}
}
public class SignatureData
{
public X509Certificate2 SigningCertificate { get; set; }
public X509Certificate2 Authority { get; set; }
public string TeamIdentifier { get; set; }
public string PassTypeIdentifier { get; set; }
}
public class EventData
{
public string Organizer { get; set; }
public string Type { get; set; }
public string Link { get; set; }
public DateTimeOffset DateTime { get; set; }
public string Title { get; set; }
public string SubTitle { get; set; }
public string Speaker { get; set; }
public EventLocation Location { get; set; }
public string Id { get; set; }
}
public class EventLocation
{
public string Address { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
}
public class Attendee
{
public string Name { get; set; }
public string Email { get; set; }
}
}
@desmphil
Copy link

desmphil commented Sep 16, 2022

Je regarde pour developper une solution complete sur azure avec function app / appel API etc.

Avez vous par hasard un projet avec du code plus recent ou une solution complete en demo?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment