2020-01-31

Building strong named assemblies in Azure Pipelines (or solving the MSB3325 error)

If you, like me, are using a certificate to sign your assemblies, making them strong named, you will get in trouble when trying to build the project on another machine (or as in this case, using an azure pipeline). MSBuild will throw an error (MSB3325) that looks like below.

Error MSB3325: Cannot import the following key file: mycertificate.pfx. 
The key file may be password protected. 
To correct this, try to import the certificate again 
or manually install the certificate to the Strong Name CSP 
with the following key container name: VS_KEY_F287A1878DD02205

Problem background

Certificates (in pfx containers) are password protected and for MSBuild to be able to use them, they must be installed in a key container. (MSBuild uses the "Microsoft Strong Cryptographic Provider", MSC)

Normally, when selecting a certificate in Visual Studio, you are prompted for the password and Visual studio creates the container for you. When the container is created, you do not need the password as long as the container exists. If you would like to build the project on another machine you will get the error above since the container is not present.

Manual solutions

If you go to another machine, one simple way to create the container is to deselect and select the certificate again (in visual studio, in the properties/signing window) which will prompt you for the password.
Another way is to use sn.exe and pass the certificate and key container name as parameters

Sn.exe -i mycertificate.pfx VS_KEY_F287A1878DD02205

This command will then prompt you for the password and create the container
https://docs.microsoft.com/en-us/dotnet/framework/tools/sn-exe-strong-name-tool

Running on Azure pipelines

To be able to build this on Azure Pipelines you need to create the container beforehand.
One way is to create a self-hosted agent that you install the container on.
https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/v2-windows?view=azure-devops

If you would like to use a Microsoft-hosted agent, then you must include the creation of the container in the pipeline itself. Using sn.exe seems like a promising way.

Automated solutions

If we would like to automate the process we have two problems that need solving
1. We need to calculate the name of the container
2. We need to pass the password to the sn.exe tool somehow.

The container name is determined by a class in Microsoft.Build.Tasks.v4.0.dll named ResolveKeySource. It will base the name upon the contents of the certificate, plus the user's domain and username that are running the MSBuild command. kdrapel posted an answer regarding this on StackOverflow

The other issue, passing the password to the sn.exe tool seemed a bit harder. There were some examples where piping it to the exe or writing a wrapper around stdin but none of them worked for me.

SnInstallPfx

Thanks to honzajscz, who also had this issue, the GitHub project SnInstallPfx solved both issues in a nice way. Both calculating the container name and allowing us to enter the password ended us up with a simple command line

SnInstallPfx.exe mycertificate.pfx mypassword

Final solution in Azure Pipelines

To include it in the pipeline I used the library function
https://docs.microsoft.com/en-us/azure/devops/pipelines/library/?view=azure-devops

In the library, I uploaded both the certificate and the sninstallpfx.exe tool.
The password I added in a variable group.

In the YML I first added the variable group

variables:
- group: 'My password'

Then the files needed to be copied to the target host using DownloadSecureFile task

- task: DownloadSecureFile@1
  displayName: Download Pfx
  name: myCertificatePfx
  inputs:
    secureFile: MyCertificate.pfx

- task: DownloadSecureFile@1
  displayName: Download sni
  name: snInstallPfx
  inputs:
    secureFile: SnInstallPfx.exe

Then added a PowerShell task where I imported the file paths and password as an environment variable and just invoked the exe

- task: PowerShell@2
  env:
    SN_INSTALL_PFX: $(snInstallPfx.secureFilePath)
    MYCERTIFICATE_PFX: $(myCertificatePfx.secureFilePath)
    MYCERTIFICATE_PFX_PASSWORD: $(myCertificatePfxPassword)
  inputs:
    targetType: 'inline'
    script: '&"$($ENV:SN_INSTALL_PFX)" "$($ENV:MYCERTIFICATE_PFX)" "$($ENV:MYCERTIFICATE_PFX_PASSWORD)"'

After a long research, I was finally able to build the project on a Microsoft hosted pipeline.

2019-12-02

Uri.ToString() automatically decodes url encoded characters

Uri.ToString() has been discussed quite a lot and its output has not always been consequent.
It can decode URL encoded characters in the URL, especially in the query part where it will be troublesome.

See the article from Anders Abel that discusses the differences between framework 4.0 and framework 4.5 https://coding.abel.nu/2014/10/beware-of-uri-tostring/

As shown in this article, more changes have been introduced int the newer frameworks.

I have done some testing by setting different targetFramework in the httpRuntime element of web.config.

Tests have been performed by creating a URI with a parameter and iterating all ASCII characters (256) and then calling ToString().

for(var i=0;i<16*16;i++) {
  var enc = $"%{i:x2}".ToUpper();
  var uri = new Uri($"https://some.site/?key={enc}");
  uri.ToString();
}

Tests show that different encodings are being decoded when other encodings are kept and the following table shows the differences between each framework where x marks the characters being decoded and a blank space shows that the encoding is kept even after ToString().



targetFramework decodedControl
00-1F

20
!
21
"
22
#
23
$
24
%
25
&
26
'()*+,
27-2C
-
2D
.
2E
/
2F
0-9
30-39
:
3A
;
3B
<
3C
=
3D
>
3E
?
3F
@
40
A-Z
41-5A
[
5B
\
5C
]
5D
^_`a-z{\|}~
5E-7E
DEL
7F
extended
80-FF
4.0 126 x x x x x x x x x x x x x x x x x x x x x x x
4.5 - 4.7.1 83 x x x x x x x x x x x x x x x
4.7.2 - 4.8 75 x x x x x x x x x x
The decoded column lists the number of characters that is decoded (of the 256 possible ASCII characters available).

Summary

ToString always decodes
%20,%22,%2D,%2E,%2F,%30-%39,%3C,%3E,%41-%5A,%5E-%7E 
 "-./0-9<>A-Z^_`a-z{\|}~

I'm not sure that I would like the %20 (whitespace) to be decoded but that's perhaps a personal preference. Of course, it's readability is improving but for programming use, I'm not a fan.

4.5-4.7.1 also decodes
%21,%27-%2C,%3A,%5B,%5D 
!'()*+,:[]

4.0 also decodes
-%1F,%24,%26,%3B,%3D,%3F,%40,%5C 
Control characters $&;=?@\

4.0 is not good since an embedded query string will be decoded since an encoded & and = (%26,%3D) in a value will be decoded.

Example

https://some.site/?key=key%3Dvalue
will be decoded to
https://some.site/?key=key=value
which will mess up the whole meaning

Workaround

If you would like to avoid decoding any URL:s you can use the following extension method.
This will use the AbsoluteUri when we are dealing with an absolute URI and the OriginalString for relative

public static string ToUrl(this Uri uri)
{
    if (uri == null) return null;
    if (uri.IsAbsoluteUri) return uri.AbsoluteUri;
    return uri.OriginalString;
}

Conclusion


  1. Don't use target framework 4.0, at least switch to 4.5.
  2. If you want to avoid encoding, don't use Uri.ToString(). It will always decode some encoded characters (even if newer frameworks are better att avoiding characters that might get you in trouble).

2018-05-02

Maintaining config files in your project

Have a problem with config files (web.config, app.config) and individual settings in a team or for your building server? Look no further. Check the video below and see if ConfigBuilder can be the solution to your problems. Available as a NuGet package (Meriworks.PowerShell.ConfigBuilder) to install in your project.

2018-04-18

Create admin user in episerver after creating new site

If you ever create a new Episerver site with a default membership provider and cannot login since you don't have any current user account in the membership database, use the SQL script below to create a user (tempadmin/tempadmin) that belongs to the WebEditors and WebAdmin groups.

Disclaimer:
The created user will have it's password in clear text in the database so after creating this user, be sure to create a real admin user and delete this one.


/**
  Will create an application user (epiadmin/epiadmin) and group (webeditors) for an asp.net identity database

**/
DECLARE @aid uniqueidentifier
DECLARE @applicationName nvarchar(235)
DECLARE @uid uniqueidentifier
DECLARE @rolename nvarchar(256)

set @applicationName=N'/'
DECLARE @username nvarchar(50)
DECLARE @password nvarchar(50)
SET @username=N'tempadmin'
SET @password=N'tempadmin'

--create application
IF( NOT EXISTS (SELECT * FROM applications where ApplicationName=@applicationName))
BEGIN
 SET @aid=NEWID()
 insert into applications values(@aid,@applicationName,null)
END

SELECT @aid=ApplicationId FROM applications where ApplicationName=@applicationName
select * from applications

--create user
IF(NOT EXISTS (SELECT * FROM users where ApplicationId=@aid and UserName = @username))
BEGIN
set @uid=NEWID()
 INSERT INTO Users VALUES(@uid,@aid,@username,0,GETDATE())
END
SELECT @uid=UserId FROM Users where ApplicationId=@aid and UserName=@username
select * from users

--create membership data
IF(NOT EXISTS (SELECT * FROM memberships where ApplicationId=@aid and UserId= @uid))
BEGIN
 INSERT INTO Memberships(UserId,ApplicationId,Password,PasswordFormat,PasswordSalt,Email,PasswordQuestion,PasswordAnswer,IsApproved,IsLockedOut,CreateDate,LastLoginDate,LastPasswordChangedDate,LastLockoutDate,FailedPasswordAttemptCount,FailedPasswordAttemptWindowStart,FailedPasswordAnswerAttemptCount,FailedPasswordAnswerAttemptWindowsStart,Comment) 
                  VALUES(@uid,@aid,@password,0,'Tc7EIFB+4dw33bRNOVJTGQ==','tempadmin@example.com',NULL,NULL,1,0,GETDATE(),GETDATE(),GETDATE(),'1754-01-01 00:00:00.000',0,'1754-01-01 00:00:00.000',0,'1754-01-01 00:00:00.000',NULL)
END
select * from memberships

SET @rolename='WebEditors'
DECLARE @roleid uniqueidentifier

IF(NOT EXISTS (SELECT * from roles where applicationId=@aid and RoleName=@rolename))
BEGIN
 SET @roleid=NEWID()
 INSERT INTO Roles(roleid,applicationid,rolename) 
 values(@roleid,@aid,@rolename)
END
SELECT @roleid=RoleId FROM Roles where ApplicationId=@aid and RoleName=@rolename
select * from roles

IF(NOT EXISTS(SELECT * FROM usersinroles where roleid=@roleid AND userid=@uid))
BEGIN
 INSERT INTO usersinroles(userid,roleid) values(@uid,@roleid)
END
select * from usersinroles

SET @rolename='WebAdmins'
IF(NOT EXISTS (SELECT * from roles where applicationId=@aid and RoleName=@rolename))
BEGIN
 SET @roleid=NEWID()
 INSERT INTO Roles(roleid,applicationid,rolename) 
 values(@roleid,@aid,@rolename)
END
SELECT @roleid=RoleId FROM Roles where ApplicationId=@aid and RoleName=@rolename
select * from roles

IF(NOT EXISTS(SELECT * FROM usersinroles where roleid=@roleid AND userid=@uid))
BEGIN
 INSERT INTO usersinroles(userid,roleid) values(@uid,@roleid)
END
select * from usersinroles

2017-11-06

Link to resource in Azure portal gives you Access denied?

Ever needed to create a link to a resource in the Azure portal and when you try it or send it to a colleague, they are faced with an "Access denied" message?

This can happen if the referred resource belongs to a subscription that not belongs to your currently selected directory.

A resource URL looks like the one below where the path to the resource is following the hash sign

https://portal.azure.com/#resource/subscriptions/aaaa.../resourceGroups/my/providers/Microsoft.Sql/servers/myserver/databases/mydb/overview


A resource belongs to a subscription (subscription id is part of the resource path) and a subscription is connected to a tenant (not part of the resource path).

When you navigate to the link, the currently active directory will be used to access the resource and if the subscription cannot be found in that directory, the "Access denied" message will be shown.

To fix this, first, check the name of the directory/tenant that the resource belongs to. (An easy way is to check what directory to use is checking the drop-down menu in the Azure portal.)

You can then create a complete URL on the following path

https://portal.azure.com/signin/index/@[directoryName]#[resourcePath] 


If the resource belongs to the d1.onmicrosoft.com directory, modify the link to go to

https://portal.azure.com/signin/index/@d1.onmicrosoft.com#resource/subscriptions/aaaa.../resourceGroups/my/providers/Microsoft.Sql/servers/myserver/databases/mydb/overview 

There you have a fully portable link that will work regardless where you currently are browsing the portal (provided that your account has access to the resource of course).

2014-12-28

Diskmaskin fixad (Whirlpool, IKEA)

Hade problem med vår diskmaskin, en inbyggd Whirlpool maskin inköpt från IKEA. När man skulle starta den så började den som vanligt, men efter någon minut så började den pipa och när man öppnade den så blinkade startlampan.

Kontrollpanelen för Whirpool DWH B00

Efter lite sökande på nätet så visade sig att man kan, utifrån antal blinkningar, utläsa en felkod och i mitt fall var det 11 blinkningar vilket enligt servicemanualen indikerade på fel "FA WI failure". Detta fel kan uppstå om vattenindikatorn inte slår på när vattnet spolas in i maskinen eller om det läcker ut vatten i maskinen och flottören slår ifrån. Efter att ha öppnat maskinen så fanns det dock inte något vatten i botten på maskinen så troligtvis så är det vattenindikatorn som inte ger rätt signal.
Vattenindikatorn sitter i botten på maskinen, under filterplåten och ser ut som ett svart litet galler. Enligt några drabbade på internet kunde man lösa detta genom att med en borste eller svamp, tvätta indikatorn från smuts el dyl. I mitt fall hjälpte inte detta något. Felet kvarstod när jag försökte köra maskinen igen.
För att verifiera att det var indikatorn som var problemet så kortslöt jag anslutningen så att den alltid gav en tillslagen signal (kretschemat i servicemanualen är till god hjälp för att förstå). (Spänning i mitten, tillslagen till höger och frånslagen till vänster)

Vattenindikatorn är markerad med WI i kretsschemat, LS6 är flottörens krets. PA6 A är kontakten till vattenindikatorn där 1,2,3 i kretsschemat motsvarar kontaktens anslutningar från v->h 2,1,3

Detta lät maskinens program komma vidare ända till fördisken, men när fördiskens vatten pumpats ut så kommer felet upp igen, då troligtvis för att indikatorn skall slå ifrån.
Dock så räckte fördiskens varma vatten för att lösa upp resterna av smutsen på indikatorn så efter jag kopplat tillbaka anslutningen till indikatorn så fungerade maskinen igen. Nu kanske den lever ett par månader till. En citronsyradisk är nog behövligt snart :)

Obs! Om du vill mecka med din egna maskin, se till att koppla bort strömmen från maskinen innan du öppnar den.

2014-02-20

Duplicate config sections in web.config and internal server errors...

There is a duplicate 'myApp' section defined, Internal Server Error

Had a breakdown moment today when two sites didn't behave. Each site had a virtual application with its own web.config.
Both sites where configured almost the same but one of the virtual applications worked and not the other.
The following error was displayed.

HTTP Error 500.19 - Internal Server Error

The requested page cannot be accessed because the related configuration data for the page is invalid.

Detailed Error Information:

Module           IIS Web Core
Notification     BeginRequest
Handler          Not yet determined
Error Code       0x800700b7
Config Error     There is a duplicate 'myapp' section defined
Config File      \\?\c:\MyApp\vapp\web.config
Requested URL    http://myapp:80/vapp/
Physical Path    c:\MyApp\
Logon Method     Not yet determined
Logon User       Not yet determined

Config Source:
    7:   <configsections>
    8:     <section name="myApp" type="MyApp.MyConfigSection, MyApp"/>
    9:   </configsections>

This configuration section was present in my site as well, like below.

<section name="myApp" type="MyApp.MyConfigSection,MyApp"/>

Removing the section from the vapp did the trick, but why did it work on the other site?

Finally after some testing and borrowing a second pair of eyes (thx robert), a small difference in the setup was detected. One of the sections where declared with the type "MyApp.MyConfigSection, MyApp" and the other with "MyApp.MyConfigSection,MyApp". Did you see it?! A small whitespace between the type and assembly names where missing.
After adjusting the typo to either keeping the whitespace or removing it on both, it started to work...