webauthn-0.1.0.0: src/Crypto/WebAuthn/Metadata/Statement/Decode.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Stability: experimental
-- This module contains functions to further decode
-- [FIDO Metadata Statement](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html)
-- IDL types defined in 'Crypto.WebAuthn.Metadata.Statement.IDL' into the Haskell-specific types defined in 'Crypto.WebAuthn.Metadata.Statement.Types'
module Crypto.WebAuthn.Metadata.Statement.Decode
( decodeMetadataStatement,
decodeAAGUID,
decodeSubjectKeyIdentifier,
decodeCertificate,
)
where
import Control.Monad (unless)
import Crypto.Hash (SHA1, digestFromByteString)
import qualified Crypto.WebAuthn.Metadata.FidoRegistry as Registry
import Crypto.WebAuthn.Metadata.Statement.Types (WebauthnAttestationType (WebauthnAttestationAttCA, WebauthnAttestationBasic))
import qualified Crypto.WebAuthn.Metadata.Statement.Types as StatementTypes
import qualified Crypto.WebAuthn.Metadata.Statement.WebIDL as StatementIDL
import qualified Crypto.WebAuthn.Metadata.UAF as UAF
import qualified Crypto.WebAuthn.Model as M
import Crypto.WebAuthn.Model.Identifier (AAGUID (AAGUID), AuthenticatorIdentifier (AuthenticatorIdentifierFido2, AuthenticatorIdentifierFidoU2F), SubjectKeyIdentifier (SubjectKeyIdentifier))
import qualified Crypto.WebAuthn.WebIDL as IDL
import Data.Bifunctor (first)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base16 as Base16
import qualified Data.ByteString.Base64 as Base64
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NE
import Data.Maybe (mapMaybe)
import Data.Singletons (SingI, sing)
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Encoding (encodeUtf8)
import qualified Data.UUID as UUID
import qualified Data.X509 as X509
-- | Decodes an 'M.AAGUID' from an [aaguid](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#dom-metadatastatement-aaguid) field of a metadata statement or an [aaguid](https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#dom-metadatablobpayloadentry-aaguid) field of a metadata service payload entry field
decodeAAGUID :: StatementIDL.AAGUID -> Either Text (AuthenticatorIdentifier 'M.Fido2)
decodeAAGUID (StatementIDL.AAGUID aaguidText) = case UUID.fromText aaguidText of
Nothing -> Left $ "Could not decode metadata aaguid: " <> aaguidText
Just aaguid -> Right $ AuthenticatorIdentifierFido2 $ AAGUID aaguid
-- | Decodes a 'M.SubjectKeyIdentifier' from an [attestationCertificateKeyIdentifiers](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#dom-metadatastatement-attestationcertificatekeyidentifiers) field of a metadata statement or an [attestationCertificateKeyIdentifiers](https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#dom-metadatablobpayloadentry-attestationcertificatekeyidentifiers) field of a metadata service payload entry
decodeSubjectKeyIdentifier :: IDL.DOMString -> Either Text (AuthenticatorIdentifier 'M.FidoU2F)
decodeSubjectKeyIdentifier subjectKeyIdentifierText = case Base16.decode (encodeUtf8 subjectKeyIdentifierText) of
Left err -> Left $ "A attestationCertificateKeyIdentifier failed to parse because it's not a valid base-16 encoding: " <> subjectKeyIdentifierText <> ", error: " <> Text.pack err
Right bytes -> case digestFromByteString @SHA1 bytes of
Nothing -> Left $ "A attestationCertificateKeyIdentifier failed to parse because it has the wrong length for a SHA1 hash: " <> subjectKeyIdentifierText
Just hash -> Right $ AuthenticatorIdentifierFidoU2F $ SubjectKeyIdentifier hash
-- | Decodes a 'X509.SignedCertificate' from an [attestationRootCertificates](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#dom-metadatastatement-attestationrootcertificates) field of a metadata statement or the [certificate](https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#dom-statusreport-certificate) field of a metadata service status report
decodeCertificate :: IDL.DOMString -> Either Text X509.SignedCertificate
decodeCertificate text =
-- TODO: Remove Text.strip, it's only needed because of a spec violation, see
-- <https://github.com/tweag/haskell-fido2/issues/68>
-- TODO: Don't use decodeLenient, it's only needed because of a spec
-- violation, see TODO above
let bytes = Base64.decodeLenient (encodeUtf8 $ Text.strip text)
in case X509.decodeSignedCertificate bytes of
Left err -> Left $ "A certificate failed to parse because it's not a valid encoding: " <> text <> ", error: " <> Text.pack err
Right certificate -> Right certificate
-- | Fully decodes a [MetadataStatement](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#metadata-keys).
-- The @p@ type parameter is the 'StatementIDL.ProtocolFamily' that this metadata statement is for.
decodeMetadataStatement ::
forall p.
SingI p =>
-- | The raw metadata statement, directly parsed from JSON
StatementIDL.MetadataStatement ->
-- | Either an early exit with 'Left', where @Left Nothing@ signals that
-- this entry can be skipped because it's not relevant for Webauthn, and
-- @Left . Just@ signals that an error happened during decoding
-- Otherwise a successful result with 'Right'
Either (Maybe Text) (StatementTypes.MetadataStatement p)
decodeMetadataStatement StatementIDL.MetadataStatement {..} = do
let msLegalHeader = legalHeader
msDescription = description
msAlternativeDescriptions = alternativeDescriptions
msAuthenticatorVersion = authenticatorVersion
unless (schema == 3) $ Left $ Just $ "Schema version is not 3 but " <> Text.pack (show schema)
msUpv <- case sing @p of
M.SFidoU2F -> first Just $ traverse decodeUpvFidoU2F upv
M.SFido2 -> first Just $ traverse decodeUpvFido2 upv
let msAuthenticationAlgorithms = authenticationAlgorithms
msPublicKeyAlgAndEncodings = publicKeyAlgAndEncodings
msAttestationTypes <- decodeAttestationTypes attestationTypes
let msUserVerificationDetails = userVerificationDetails
msKeyProtection = keyProtection
msIsKeyRestricted = isKeyRestricted
msIsFreshUserVerificationRequired = isFreshUserVerificationRequired
msMatcherProtection = matcherProtection
msCryptoStrength = cryptoStrength
msAttachmentHint = attachmentHint
msTcDisplay = tcDisplay
msTcDisplayContentType = tcDisplayContentType
msTcDisplayPNGCharacteristics = tcDisplayPNGCharacteristics
msAttestationRootCertificates <- case NE.nonEmpty attestationRootCertificates of
-- > When supporting surrogate basic attestation only, no attestation trust anchor is required/used. So this array MUST be empty in that case.
-- This will never be the case, because if only surrogate basic attestation is used, then decodeAttestationTypes above will have returned (Left Nothing) already
Nothing -> Left $ Just "attestationRootCertificates should not be empty"
Just certs -> first Just $ traverse decodeCertificate certs
msIcon <- first Just $ traverse decodeIcon icon
let msSupportedExtensions = supportedExtensions
msAuthenticatorGetInfo = authenticatorGetInfo
pure $ StatementTypes.MetadataStatement {..}
where
decodeUpvFidoU2F :: UAF.Version -> Either Text (StatementTypes.ProtocolVersion 'M.FidoU2F)
decodeUpvFidoU2F UAF.Version {UAF.major = 1, UAF.minor = 0} = Right StatementTypes.U2F1_0
decodeUpvFidoU2F UAF.Version {UAF.major = 1, UAF.minor = 1} = Right StatementTypes.U2F1_1
decodeUpvFidoU2F UAF.Version {UAF.major = 1, UAF.minor = 2} = Right StatementTypes.U2F1_2
decodeUpvFidoU2F version = Left $ "Unknown FIDO U2F UPV version: " <> Text.pack (show version)
decodeUpvFido2 :: UAF.Version -> Either Text (StatementTypes.ProtocolVersion 'M.Fido2)
decodeUpvFido2 UAF.Version {UAF.major = 1, UAF.minor = 0} = Right StatementTypes.CTAP2_0
decodeUpvFido2 UAF.Version {UAF.major = 1, UAF.minor = 1} = Right StatementTypes.CTAP2_1
decodeUpvFido2 version = Left $ "Unknown FIDO2 UPV version: " <> Text.pack (show version)
-- Turns a non-empty list of 'Registry.AuthenticatorAttestationType' into a non-empty list of 'WebauthnAttestationType'.
-- If the authenticator doesn't support any webauthn attestation types,
-- `Left Nothing` is returned, indicating that this authenticator should be ignored
decodeAttestationTypes ::
NonEmpty Registry.AuthenticatorAttestationType ->
Either (Maybe Text) (NonEmpty WebauthnAttestationType)
decodeAttestationTypes types = case NE.nonEmpty $ mapMaybe transform $ NE.toList types of
Nothing -> Left Nothing
Just result -> Right result
where
transform :: Registry.AuthenticatorAttestationType -> Maybe WebauthnAttestationType
transform Registry.ATTESTATION_BASIC_FULL = Just WebauthnAttestationBasic
transform Registry.ATTESTATION_ATTCA = Just WebauthnAttestationAttCA
transform _ = Nothing
-- Decodes the PNG bytes of an [icon](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#dom-metadatastatement-icon) field of a metadata statement
decodeIcon :: IDL.DOMString -> Either Text BS.ByteString
decodeIcon dataUrl = case Text.stripPrefix "data:image/png;base64," dataUrl of
Nothing -> Left $ "Icon decoding failed because there is no \"data:image/png;base64,\" prefix: " <> dataUrl
Just suffix ->
-- TODO: Use non-lenient decoding, it's only needed because of a spec violation,
-- see <https://github.com/tweag/haskell-fido2/issues/68>
Right $ Base64.decodeLenient (encodeUtf8 suffix)