2021-02-28 18:09:06 +00:00
package acme
2021-02-28 01:05:37 +00:00
import (
"context"
"crypto"
2022-09-01 17:45:31 +00:00
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
2021-02-28 01:05:37 +00:00
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
2022-04-08 20:49:11 +00:00
"crypto/x509"
2021-02-28 01:05:37 +00:00
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"encoding/json"
2021-08-18 00:06:25 +00:00
"errors"
2021-02-28 01:05:37 +00:00
"fmt"
2021-11-12 23:46:34 +00:00
"io"
2021-02-28 01:05:37 +00:00
"net"
2021-03-30 05:58:26 +00:00
"net/url"
2021-08-18 00:06:25 +00:00
"reflect"
2022-08-30 02:37:30 +00:00
"strconv"
2021-02-28 01:05:37 +00:00
"strings"
"time"
2022-04-08 20:49:11 +00:00
"github.com/fxamacker/cbor/v2"
2023-09-19 10:11:46 +00:00
"github.com/google/go-tpm/legacy/tpm2"
2023-05-09 22:38:40 +00:00
"github.com/smallstep/go-attestation/attest"
2021-02-28 01:05:37 +00:00
"go.step.sm/crypto/jose"
2023-02-10 00:48:43 +00:00
"go.step.sm/crypto/keyutil"
2022-07-15 00:10:03 +00:00
"go.step.sm/crypto/pemutil"
2023-03-31 15:39:18 +00:00
"go.step.sm/crypto/x509util"
2024-01-08 21:21:27 +00:00
"golang.org/x/exp/slices"
2022-09-21 12:58:03 +00:00
2024-01-11 10:06:39 +00:00
"github.com/smallstep/certificates/acme/wire"
2022-09-21 12:58:03 +00:00
"github.com/smallstep/certificates/authority/provisioner"
2021-02-28 01:05:37 +00:00
)
2021-06-18 10:39:36 +00:00
type ChallengeType string
const (
2021-11-12 16:13:10 +00:00
// HTTP01 is the http-01 ACME challenge type
HTTP01 ChallengeType = "http-01"
// DNS01 is the dns-01 ACME challenge type
DNS01 ChallengeType = "dns-01"
// TLSALPN01 is the tls-alpn-01 ACME challenge type
2021-06-18 10:39:36 +00:00
TLSALPN01 ChallengeType = "tls-alpn-01"
2022-04-02 02:56:05 +00:00
// DEVICEATTEST01 is the device-attest-01 ACME challenge type
DEVICEATTEST01 ChallengeType = "device-attest-01"
2022-12-08 17:10:36 +00:00
// WIREOIDC01 is the Wire OIDC challenge type
WIREOIDC01 ChallengeType = "wire-oidc-01"
// WIREDPOP01 is the Wire DPoP challenge type
WIREDPOP01 ChallengeType = "wire-dpop-01"
2021-06-18 10:39:36 +00:00
)
2022-11-03 23:58:25 +00:00
var (
// InsecurePortHTTP01 is the port used to verify http-01 challenges. If not set it
// defaults to 80.
InsecurePortHTTP01 int
// InsecurePortTLSALPN01 is the port used to verify tls-alpn-01 challenges. If not
// set it defaults to 443.
//
// This variable can be used for testing purposes.
InsecurePortTLSALPN01 int
)
2021-02-28 01:05:37 +00:00
// Challenge represents an ACME response Challenge type.
type Challenge struct {
2021-06-18 10:39:36 +00:00
ID string ` json:"-" `
AccountID string ` json:"-" `
AuthorizationID string ` json:"-" `
Value string ` json:"-" `
Type ChallengeType ` json:"type" `
Status Status ` json:"status" `
Token string ` json:"token" `
ValidatedAt string ` json:"validated,omitempty" `
URL string ` json:"url" `
2024-01-09 15:43:18 +00:00
Target string ` json:"target,omitempty" `
2021-06-18 10:39:36 +00:00
Error * Error ` json:"error,omitempty" `
2021-02-28 01:05:37 +00:00
}
// ToLog enables response logging.
func ( ch * Challenge ) ToLog ( ) ( interface { } , error ) {
b , err := json . Marshal ( ch )
if err != nil {
2021-03-05 07:10:46 +00:00
return nil , WrapErrorISE ( err , "error marshaling challenge for logging" )
2021-02-28 01:05:37 +00:00
}
return string ( b ) , nil
}
2023-01-27 14:36:48 +00:00
// Validate attempts to validate the Challenge. Stores changes to the Challenge
// type using the DB interface. If the Challenge is validated, the 'status' and
// 'validated' attributes are updated.
2022-04-02 02:56:05 +00:00
func ( ch * Challenge ) Validate ( ctx context . Context , db DB , jwk * jose . JSONWebKey , payload [ ] byte ) error {
2021-02-28 01:05:37 +00:00
// If already valid or invalid then return without performing validation.
2021-03-30 05:58:26 +00:00
if ch . Status != StatusPending {
2021-02-28 01:05:37 +00:00
return nil
}
switch ch . Type {
2021-06-18 10:39:36 +00:00
case HTTP01 :
2022-04-29 02:15:18 +00:00
return http01Validate ( ctx , ch , db , jwk )
2021-06-18 10:39:36 +00:00
case DNS01 :
2022-04-29 02:15:18 +00:00
return dns01Validate ( ctx , ch , db , jwk )
2021-06-18 10:39:36 +00:00
case TLSALPN01 :
2022-04-29 02:15:18 +00:00
return tlsalpn01Validate ( ctx , ch , db , jwk )
2022-04-02 02:56:05 +00:00
case DEVICEATTEST01 :
return deviceAttest01Validate ( ctx , ch , db , jwk , payload )
2022-12-08 17:10:36 +00:00
case WIREOIDC01 :
return wireOIDC01Validate ( ctx , ch , db , jwk , payload )
case WIREDPOP01 :
return wireDPOP01Validate ( ctx , ch , db , jwk , payload )
2021-02-28 01:05:37 +00:00
default :
2021-03-05 07:10:46 +00:00
return NewErrorISE ( "unexpected challenge type '%s'" , ch . Type )
2021-02-28 01:05:37 +00:00
}
}
2022-04-29 02:15:18 +00:00
func http01Validate ( ctx context . Context , ch * Challenge , db DB , jwk * jose . JSONWebKey ) error {
2022-04-07 10:37:34 +00:00
u := & url . URL { Scheme : "http" , Host : http01ChallengeHost ( ch . Value ) , Path : fmt . Sprintf ( "/.well-known/acme-challenge/%s" , ch . Token ) }
2021-02-28 01:05:37 +00:00
2022-11-03 23:58:25 +00:00
// Append insecure port if set.
// Only used for testing purposes.
if InsecurePortHTTP01 != 0 {
u . Host += ":" + strconv . Itoa ( InsecurePortHTTP01 )
}
2022-04-29 02:15:18 +00:00
vc := MustClientFromContext ( ctx )
resp , err := vc . Get ( u . String ( ) )
2021-02-28 01:05:37 +00:00
if err != nil {
2021-03-30 05:58:26 +00:00
return storeError ( ctx , db , ch , false , WrapError ( ErrorConnectionType , err ,
2021-10-08 18:59:57 +00:00
"error doing http GET for url %s" , u ) )
2021-02-28 01:05:37 +00:00
}
2021-03-30 05:58:26 +00:00
defer resp . Body . Close ( )
2021-02-28 01:05:37 +00:00
if resp . StatusCode >= 400 {
2021-03-30 05:58:26 +00:00
return storeError ( ctx , db , ch , false , NewError ( ErrorConnectionType ,
2021-10-08 18:59:57 +00:00
"error doing http GET for url %s with status code %d" , u , resp . StatusCode ) )
2021-02-28 01:05:37 +00:00
}
2021-11-12 23:46:34 +00:00
body , err := io . ReadAll ( resp . Body )
2021-02-28 01:05:37 +00:00
if err != nil {
2021-03-05 07:10:46 +00:00
return WrapErrorISE ( err , "error reading " +
2021-10-08 18:59:57 +00:00
"response body for url %s" , u )
2021-02-28 01:05:37 +00:00
}
2021-03-30 05:58:26 +00:00
keyAuth := strings . TrimSpace ( string ( body ) )
2021-02-28 01:05:37 +00:00
expected , err := KeyAuthorization ( ch . Token , jwk )
if err != nil {
return err
}
if keyAuth != expected {
2021-03-30 05:58:26 +00:00
return storeError ( ctx , db , ch , true , NewError ( ErrorRejectedIdentifierType ,
2021-03-01 07:33:18 +00:00
"keyAuthorization does not match; expected %s, but got %s" , expected , keyAuth ) )
2021-02-28 01:05:37 +00:00
}
// Update and store the challenge.
ch . Status = StatusValid
ch . Error = nil
2021-03-19 06:08:13 +00:00
ch . ValidatedAt = clock . Now ( ) . Format ( time . RFC3339 )
2021-02-28 01:05:37 +00:00
2021-03-01 07:33:18 +00:00
if err = db . UpdateChallenge ( ctx , ch ) ; err != nil {
2021-03-05 07:10:46 +00:00
return WrapErrorISE ( err , "error updating challenge" )
2021-03-01 07:33:18 +00:00
}
return nil
2021-02-28 01:05:37 +00:00
}
2022-04-07 10:37:34 +00:00
// http01ChallengeHost checks if a Challenge value is an IPv6 address
// and adds square brackets if that's the case, so that it can be used
// as a hostname. Returns the original Challenge value as the host to
// use in other cases.
func http01ChallengeHost ( value string ) string {
if ip := net . ParseIP ( value ) ; ip != nil && ip . To4 ( ) == nil {
value = "[" + value + "]"
}
return value
}
2021-08-18 00:06:25 +00:00
func tlsAlert ( err error ) uint8 {
var opErr * net . OpError
if errors . As ( err , & opErr ) {
v := reflect . ValueOf ( opErr . Err )
if v . Kind ( ) == reflect . Uint8 {
return uint8 ( v . Uint ( ) )
}
}
return 0
}
2022-04-29 02:15:18 +00:00
func tlsalpn01Validate ( ctx context . Context , ch * Challenge , db DB , jwk * jose . JSONWebKey ) error {
2021-02-28 01:05:37 +00:00
config := & tls . Config {
2021-03-30 05:58:26 +00:00
NextProtos : [ ] string { "acme-tls/1" } ,
// https://tools.ietf.org/html/rfc8737#section-4
// ACME servers that implement "acme-tls/1" MUST only negotiate TLS 1.2
// [RFC5246] or higher when connecting to clients for validation.
MinVersion : tls . VersionTLS12 ,
2021-06-18 15:27:35 +00:00
ServerName : serverName ( ch ) ,
2022-09-21 04:01:55 +00:00
InsecureSkipVerify : true , //nolint:gosec // we expect a self-signed challenge certificate
2021-02-28 01:05:37 +00:00
}
2022-11-03 23:58:25 +00:00
var hostPort string
// Allow to change TLS port for testing purposes.
if port := InsecurePortTLSALPN01 ; port == 0 {
hostPort = net . JoinHostPort ( ch . Value , "443" )
} else {
hostPort = net . JoinHostPort ( ch . Value , strconv . Itoa ( port ) )
}
2021-02-28 01:05:37 +00:00
2022-04-29 02:15:18 +00:00
vc := MustClientFromContext ( ctx )
conn , err := vc . TLSDial ( "tcp" , hostPort , config )
2021-02-28 01:05:37 +00:00
if err != nil {
2021-08-17 23:31:53 +00:00
// With Go 1.17+ tls.Dial fails if there's no overlap between configured
2021-08-18 00:06:25 +00:00
// client and server protocols. When this happens the connection is
// closed with the error no_application_protocol(120) as required by
// RFC7301. See https://golang.org/doc/go1.17#ALPN
if tlsAlert ( err ) == 120 {
2021-08-17 23:31:53 +00:00
return storeError ( ctx , db , ch , true , NewError ( ErrorRejectedIdentifierType ,
"cannot negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge" ) )
}
2021-03-30 05:58:26 +00:00
return storeError ( ctx , db , ch , false , WrapError ( ErrorConnectionType , err ,
2021-03-01 07:33:18 +00:00
"error doing TLS dial for %s" , hostPort ) )
2021-02-28 01:05:37 +00:00
}
defer conn . Close ( )
cs := conn . ConnectionState ( )
certs := cs . PeerCertificates
if len ( certs ) == 0 {
2021-03-30 05:58:26 +00:00
return storeError ( ctx , db , ch , true , NewError ( ErrorRejectedIdentifierType ,
2021-03-01 07:33:18 +00:00
"%s challenge for %s resulted in no certificates" , ch . Type , ch . Value ) )
2021-02-28 01:05:37 +00:00
}
2021-04-13 21:53:05 +00:00
if cs . NegotiatedProtocol != "acme-tls/1" {
2021-03-30 05:58:26 +00:00
return storeError ( ctx , db , ch , true , NewError ( ErrorRejectedIdentifierType ,
2021-03-01 07:33:18 +00:00
"cannot negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge" ) )
2021-02-28 01:05:37 +00:00
}
leafCert := certs [ 0 ]
2021-05-28 22:19:14 +00:00
// if no DNS names present, look for IP address and verify that exactly one exists
if len ( leafCert . DNSNames ) == 0 {
2021-06-18 15:27:35 +00:00
if len ( leafCert . IPAddresses ) != 1 || ! leafCert . IPAddresses [ 0 ] . Equal ( net . ParseIP ( ch . Value ) ) {
2021-05-28 22:19:14 +00:00
return storeError ( ctx , db , ch , true , NewError ( ErrorRejectedIdentifierType ,
2021-06-04 06:42:24 +00:00
"incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single IP address or DNS name, %v" , ch . Value ) )
2021-05-28 22:19:14 +00:00
}
} else {
if len ( leafCert . DNSNames ) != 1 || ! strings . EqualFold ( leafCert . DNSNames [ 0 ] , ch . Value ) {
return storeError ( ctx , db , ch , true , NewError ( ErrorRejectedIdentifierType ,
2021-06-04 06:42:24 +00:00
"incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single IP address or DNS name, %v" , ch . Value ) )
2021-05-28 22:19:14 +00:00
}
2021-02-28 01:05:37 +00:00
}
idPeAcmeIdentifier := asn1 . ObjectIdentifier { 1 , 3 , 6 , 1 , 5 , 5 , 7 , 1 , 31 }
idPeAcmeIdentifierV1Obsolete := asn1 . ObjectIdentifier { 1 , 3 , 6 , 1 , 5 , 5 , 7 , 1 , 30 , 1 }
foundIDPeAcmeIdentifierV1Obsolete := false
2021-03-01 06:49:20 +00:00
keyAuth , err := KeyAuthorization ( ch . Token , jwk )
2021-02-28 01:05:37 +00:00
if err != nil {
2021-03-01 06:49:20 +00:00
return err
2021-02-28 01:05:37 +00:00
}
hashedKeyAuth := sha256 . Sum256 ( [ ] byte ( keyAuth ) )
for _ , ext := range leafCert . Extensions {
if idPeAcmeIdentifier . Equal ( ext . Id ) {
if ! ext . Critical {
2021-03-30 05:58:26 +00:00
return storeError ( ctx , db , ch , true , NewError ( ErrorRejectedIdentifierType ,
2021-03-01 07:33:18 +00:00
"incorrect certificate for tls-alpn-01 challenge: acmeValidationV1 extension not critical" ) )
2021-02-28 01:05:37 +00:00
}
var extValue [ ] byte
rest , err := asn1 . Unmarshal ( ext . Value , & extValue )
if err != nil || len ( rest ) > 0 || len ( hashedKeyAuth ) != len ( extValue ) {
2021-03-30 05:58:26 +00:00
return storeError ( ctx , db , ch , true , NewError ( ErrorRejectedIdentifierType ,
2021-03-01 07:33:18 +00:00
"incorrect certificate for tls-alpn-01 challenge: malformed acmeValidationV1 extension value" ) )
2021-02-28 01:05:37 +00:00
}
if subtle . ConstantTimeCompare ( hashedKeyAuth [ : ] , extValue ) != 1 {
2021-03-30 05:58:26 +00:00
return storeError ( ctx , db , ch , true , NewError ( ErrorRejectedIdentifierType ,
2021-03-01 07:33:18 +00:00
"incorrect certificate for tls-alpn-01 challenge: " +
"expected acmeValidationV1 extension value %s for this challenge but got %s" ,
hex . EncodeToString ( hashedKeyAuth [ : ] ) , hex . EncodeToString ( extValue ) ) )
2021-02-28 01:05:37 +00:00
}
ch . Status = StatusValid
ch . Error = nil
2021-03-19 06:08:13 +00:00
ch . ValidatedAt = clock . Now ( ) . Format ( time . RFC3339 )
2021-02-28 01:05:37 +00:00
2021-03-01 06:49:20 +00:00
if err = db . UpdateChallenge ( ctx , ch ) ; err != nil {
2021-03-05 07:10:46 +00:00
return WrapErrorISE ( err , "tlsalpn01ValidateChallenge - error updating challenge" )
2021-03-01 06:49:20 +00:00
}
return nil
2021-02-28 01:05:37 +00:00
}
if idPeAcmeIdentifierV1Obsolete . Equal ( ext . Id ) {
foundIDPeAcmeIdentifierV1Obsolete = true
}
}
if foundIDPeAcmeIdentifierV1Obsolete {
2021-03-30 05:58:26 +00:00
return storeError ( ctx , db , ch , true , NewError ( ErrorRejectedIdentifierType ,
2021-03-01 07:33:18 +00:00
"incorrect certificate for tls-alpn-01 challenge: obsolete id-pe-acmeIdentifier in acmeValidationV1 extension" ) )
2021-02-28 01:05:37 +00:00
}
2021-03-30 05:58:26 +00:00
return storeError ( ctx , db , ch , true , NewError ( ErrorRejectedIdentifierType ,
2021-03-01 07:33:18 +00:00
"incorrect certificate for tls-alpn-01 challenge: missing acmeValidationV1 extension" ) )
2021-02-28 01:05:37 +00:00
}
2022-04-29 02:15:18 +00:00
func dns01Validate ( ctx context . Context , ch * Challenge , db DB , jwk * jose . JSONWebKey ) error {
2021-02-28 01:05:37 +00:00
// Normalize domain for wildcard DNS names
// This is done to avoid making TXT lookups for domains like
// _acme-challenge.*.example.com
// Instead perform txt lookup for _acme-challenge.example.com
2021-03-01 06:49:20 +00:00
domain := strings . TrimPrefix ( ch . Value , "*." )
2021-02-28 01:05:37 +00:00
2022-04-29 02:15:18 +00:00
vc := MustClientFromContext ( ctx )
txtRecords , err := vc . LookupTxt ( "_acme-challenge." + domain )
2021-02-28 01:05:37 +00:00
if err != nil {
2021-03-30 05:58:26 +00:00
return storeError ( ctx , db , ch , false , WrapError ( ErrorDNSType , err ,
2021-03-01 07:33:18 +00:00
"error looking up TXT records for domain %s" , domain ) )
2021-02-28 01:05:37 +00:00
}
2021-03-01 06:49:20 +00:00
expectedKeyAuth , err := KeyAuthorization ( ch . Token , jwk )
2021-02-28 01:05:37 +00:00
if err != nil {
2021-03-01 06:49:20 +00:00
return err
2021-02-28 01:05:37 +00:00
}
h := sha256 . Sum256 ( [ ] byte ( expectedKeyAuth ) )
expected := base64 . RawURLEncoding . EncodeToString ( h [ : ] )
var found bool
for _ , r := range txtRecords {
if r == expected {
found = true
break
}
}
if ! found {
2021-03-30 05:58:26 +00:00
return storeError ( ctx , db , ch , false , NewError ( ErrorRejectedIdentifierType ,
2021-03-01 07:33:18 +00:00
"keyAuthorization does not match; expected %s, but got %s" , expectedKeyAuth , txtRecords ) )
2021-02-28 01:05:37 +00:00
}
// Update and store the challenge.
ch . Status = StatusValid
ch . Error = nil
2021-03-19 06:08:13 +00:00
ch . ValidatedAt = clock . Now ( ) . Format ( time . RFC3339 )
2021-02-28 01:05:37 +00:00
2021-03-01 07:33:18 +00:00
if err = db . UpdateChallenge ( ctx , ch ) ; err != nil {
2021-03-05 07:10:46 +00:00
return WrapErrorISE ( err , "error updating challenge" )
2021-03-01 07:33:18 +00:00
}
return nil
2021-02-28 01:05:37 +00:00
}
2024-01-10 14:12:28 +00:00
type wireOidcPayload struct {
// IDToken contains the OIDC identity token
2024-01-09 20:31:10 +00:00
IDToken string ` json:"id_token,omitempty" `
2023-02-23 18:07:12 +00:00
// KeyAuth ({challenge-token}.{jwk-thumbprint})
KeyAuth string ` json:"keyauth,omitempty" `
2023-02-02 09:27:23 +00:00
}
2022-12-08 17:10:36 +00:00
func wireOIDC01Validate ( ctx context . Context , ch * Challenge , db DB , jwk * jose . JSONWebKey , payload [ ] byte ) error {
prov , ok := ProvisionerFromContext ( ctx )
if ! ok {
return NewErrorISE ( "no provisioner provided" )
}
2024-01-10 14:12:28 +00:00
var oidcPayload wireOidcPayload
err := json . Unmarshal ( payload , & oidcPayload )
2023-02-02 09:27:23 +00:00
if err != nil {
return storeError ( ctx , db , ch , false , WrapError ( ErrorRejectedIdentifierType , err ,
"error unmarshalling Wire challenge payload" ) )
}
2024-01-11 12:18:43 +00:00
oidcOptions := prov . GetOptions ( ) . GetWireOptions ( ) . GetOIDCOptions ( )
2024-01-10 14:12:28 +00:00
idToken , err := oidcOptions . GetProvider ( ctx ) . Verifier ( oidcOptions . GetConfig ( ) ) . Verify ( ctx , oidcPayload . IDToken )
2022-12-08 17:10:36 +00:00
if err != nil {
return storeError ( ctx , db , ch , false , WrapError ( ErrorRejectedIdentifierType , err ,
"error verifying ID token signature" ) )
}
var claims struct {
2023-03-30 12:33:04 +00:00
Name string ` json:"preferred_username,omitempty" `
Handle string ` json:"name" `
Issuer string ` json:"iss,omitempty" `
GivenName string ` json:"given_name,omitempty" `
2024-01-10 17:32:18 +00:00
KeyAuth string ` json:"keyauth" ` // TODO(hs): use this property instead of the one in the payload after https://github.com/wireapp/rusty-jwt-tools/tree/fix/keyauth is done
2022-12-08 17:10:36 +00:00
}
2024-01-11 11:03:52 +00:00
if err := idToken . Claims ( & claims ) ; err != nil {
2022-12-08 17:10:36 +00:00
return storeError ( ctx , db , ch , false , WrapError ( ErrorRejectedIdentifierType , err ,
"error retrieving claims from ID token" ) )
}
2024-01-11 12:47:17 +00:00
wireID , err := wire . ParseID ( [ ] byte ( ch . Value ) )
2022-12-08 17:10:36 +00:00
if err != nil {
return WrapErrorISE ( err , "error unmarshalling challenge data" )
}
expectedKeyAuth , err := KeyAuthorization ( ch . Token , jwk )
if err != nil {
return err
}
2024-01-10 14:12:28 +00:00
if expectedKeyAuth != oidcPayload . KeyAuth {
2022-12-08 17:10:36 +00:00
return storeError ( ctx , db , ch , true , NewError ( ErrorRejectedIdentifierType ,
2024-01-11 12:47:17 +00:00
"keyAuthorization does not match; expected %q, but got %q" , expectedKeyAuth , oidcPayload . KeyAuth ) )
2022-12-08 17:10:36 +00:00
}
2024-01-11 12:47:17 +00:00
if wireID . Name != claims . Name || wireID . Handle != claims . Handle {
return storeError ( ctx , db , ch , false , NewError ( ErrorRejectedIdentifierType , "claims in OIDC ID token don't match" ) )
2022-12-08 17:10:36 +00:00
}
2023-01-31 16:36:06 +00:00
// Update and store the challenge.
ch . Status = StatusValid
ch . Error = nil
ch . ValidatedAt = clock . Now ( ) . Format ( time . RFC3339 )
if err = db . UpdateChallenge ( ctx , ch ) ; err != nil {
return WrapErrorISE ( err , "error updating challenge" )
}
2023-05-22 13:51:02 +00:00
2024-01-11 11:03:52 +00:00
parsedIDToken , err := jose . ParseSigned ( oidcPayload . IDToken )
2023-05-22 13:51:02 +00:00
if err != nil {
2024-01-11 10:06:39 +00:00
return WrapErrorISE ( err , "invalid OIDC ID token" )
2023-05-22 13:51:02 +00:00
}
oidcToken := make ( map [ string ] interface { } )
2024-01-09 20:31:10 +00:00
if err := parsedIDToken . UnsafeClaimsWithoutVerification ( & oidcToken ) ; err != nil {
2024-01-10 14:12:28 +00:00
return WrapErrorISE ( err , "failed parsing OIDC id token" )
2023-05-22 13:51:02 +00:00
}
orders , err := db . GetAllOrdersByAccountID ( ctx , ch . AccountID )
if err != nil {
2024-01-10 14:12:28 +00:00
return WrapErrorISE ( err , "could not find current order by account id" )
2023-05-22 13:51:02 +00:00
}
2023-09-12 12:15:24 +00:00
if len ( orders ) == 0 {
2024-01-11 10:06:39 +00:00
return NewErrorISE ( "there are not enough orders for this account for this custom OIDC challenge" )
2023-05-26 08:23:24 +00:00
}
2023-09-12 14:00:53 +00:00
order := orders [ len ( orders ) - 1 ]
2023-09-12 12:15:24 +00:00
if err := db . CreateOidcToken ( ctx , order , oidcToken ) ; err != nil {
2024-01-10 14:12:28 +00:00
return WrapErrorISE ( err , "failed storing OIDC id token" )
2023-05-22 13:51:02 +00:00
}
2022-12-08 17:10:36 +00:00
return nil
}
2024-01-10 14:12:28 +00:00
type wireDpopPayload struct {
// AccessToken is the token generated by wire-server
AccessToken string ` json:"access_token,omitempty" `
}
2022-12-08 17:10:36 +00:00
func wireDPOP01Validate ( ctx context . Context , ch * Challenge , db DB , jwk * jose . JSONWebKey , payload [ ] byte ) error {
2024-01-09 20:31:10 +00:00
prov , ok := ProvisionerFromContext ( ctx )
2022-12-08 17:10:36 +00:00
if ! ok {
return NewErrorISE ( "missing provisioner" )
}
2024-01-11 10:06:39 +00:00
rawKid , err := jwk . Thumbprint ( crypto . SHA256 )
if err != nil {
return storeError ( ctx , db , ch , false , WrapError ( ErrorServerInternalType , err , "failed to compute JWK thumbprint" ) )
2023-07-28 13:16:59 +00:00
}
2023-07-28 14:47:11 +00:00
kid := base64 . RawURLEncoding . EncodeToString ( rawKid )
2023-07-28 14:19:12 +00:00
2024-01-10 14:12:28 +00:00
var dpopPayload wireDpopPayload
2024-01-11 11:03:52 +00:00
if err := json . Unmarshal ( payload , & dpopPayload ) ; err != nil {
2023-02-02 09:27:23 +00:00
return storeError ( ctx , db , ch , false , WrapError ( ErrorRejectedIdentifierType , err ,
"error unmarshalling Wire challenge payload" ) )
}
2024-01-10 14:12:28 +00:00
wireID , err := wire . ParseID ( [ ] byte ( ch . Value ) )
2022-12-08 17:10:36 +00:00
if err != nil {
return WrapErrorISE ( err , "error unmarshalling challenge data" )
}
2024-01-10 14:12:28 +00:00
clientID , err := wire . ParseClientID ( wireID . ClientID )
2023-05-05 13:39:40 +00:00
if err != nil {
return WrapErrorISE ( err , "error parsing device id" )
}
2024-01-11 12:18:43 +00:00
dpopOptions := prov . GetOptions ( ) . GetWireOptions ( ) . GetDPOPOptions ( )
2024-01-10 14:12:28 +00:00
2023-05-05 13:39:40 +00:00
issuer , err := dpopOptions . GetTarget ( clientID . DeviceID )
if err != nil {
2024-01-10 14:12:28 +00:00
return WrapErrorISE ( err , "invalid Go template registered for 'target'" )
2023-05-05 13:39:40 +00:00
}
2023-05-04 15:29:19 +00:00
2024-01-10 14:12:28 +00:00
key := dpopOptions . GetSigningKey ( )
2024-01-11 11:42:43 +00:00
params := verifyParams {
token : dpopPayload . AccessToken ,
key : key ,
kid : kid ,
issuer : issuer ,
wireID : wireID ,
challenge : ch ,
t : clock . Now ( ) . UTC ( ) ,
}
_ , dpop , err := parseAndVerifyWireAccessToken ( params )
2022-12-08 17:10:36 +00:00
if err != nil {
2024-01-10 14:12:28 +00:00
return WrapErrorISE ( err , "failed validating token" )
2022-12-08 17:10:36 +00:00
}
2024-01-10 14:12:28 +00:00
// Update and store the challenge.
ch . Status = StatusValid
ch . Error = nil
ch . ValidatedAt = clock . Now ( ) . Format ( time . RFC3339 )
if err = db . UpdateChallenge ( ctx , ch ) ; err != nil {
return WrapErrorISE ( err , "error updating challenge" )
2022-12-08 17:10:36 +00:00
}
2024-01-10 14:12:28 +00:00
orders , err := db . GetAllOrdersByAccountID ( ctx , ch . AccountID )
2022-12-08 17:10:36 +00:00
if err != nil {
2024-01-10 14:12:28 +00:00
return WrapErrorISE ( err , "could not find current order by account id" )
}
if len ( orders ) == 0 {
return NewErrorISE ( "there are not enough orders for this account for this custom OIDC challenge" )
2022-12-08 17:10:36 +00:00
}
2024-01-10 14:12:28 +00:00
order := orders [ len ( orders ) - 1 ]
if err := db . CreateDpopToken ( ctx , order , map [ string ] any ( * dpop ) ) ; err != nil {
return WrapErrorISE ( err , "failed storing DPoP token" )
2022-12-08 17:10:36 +00:00
}
2024-01-10 14:12:28 +00:00
return nil
}
type cnf struct {
Kid string ` json:"kid,omitempty" `
}
type wireAccessToken struct {
jose . Claims
Challenge string ` json:"chal,omitempty" `
Cnf * cnf ` json:"cnf,omitempty" `
Proof string ` json:"proof,omitempty" `
ClientID string ` json:"client_id,omitempty" `
APIVersion int ` json:"api_version,omitempty" `
Scope string ` json:"scope,omitempty" `
}
type wireDpopToken map [ string ] any
2024-01-11 11:42:43 +00:00
type verifyParams struct {
token string
key string
issuer string
kid string
wireID wire . ID
challenge * Challenge
t time . Time
}
func parseAndVerifyWireAccessToken ( v verifyParams ) ( * wireAccessToken , * wireDpopToken , error ) {
k , err := pemutil . Parse ( [ ] byte ( v . key ) ) // TODO(hs): move this to earlier in the configuration process? Do it once?
2022-12-08 17:10:36 +00:00
if err != nil {
2024-01-10 14:12:28 +00:00
return nil , nil , fmt . Errorf ( "failed parsing public key: %w" , err )
2022-12-08 17:10:36 +00:00
}
2024-01-10 14:12:28 +00:00
pk , ok := k . ( ed25519 . PublicKey ) // TODO(hs): allow more key types
if ! ok {
return nil , nil , fmt . Errorf ( "unexpected type: %T" , k )
2023-01-31 16:36:06 +00:00
}
2023-05-12 14:12:20 +00:00
2024-01-11 11:42:43 +00:00
jwt , err := jose . ParseSigned ( v . token )
2023-05-12 14:12:20 +00:00
if err != nil {
2024-01-10 14:12:28 +00:00
return nil , nil , fmt . Errorf ( "failed parsing token: %w" , err )
2023-05-12 14:12:20 +00:00
}
2024-01-10 14:12:28 +00:00
var accessToken wireAccessToken
if err = jwt . Claims ( pk , & accessToken ) ; err != nil {
return nil , nil , fmt . Errorf ( "failed getting token claims: %w" , err )
2023-05-12 14:12:20 +00:00
}
2024-01-10 14:12:28 +00:00
if err := accessToken . ValidateWithLeeway ( jose . Expected {
2024-01-11 11:42:43 +00:00
Time : v . t ,
Issuer : v . issuer ,
2024-01-10 14:12:28 +00:00
} , 360 * time . Second ) ; err != nil {
return nil , nil , fmt . Errorf ( "failed validation: %w" , err )
2023-05-16 12:33:11 +00:00
}
2024-01-10 14:12:28 +00:00
if accessToken . Cnf == nil {
return nil , nil , errors . New ( "'cnf' is nil" )
2023-05-16 12:33:11 +00:00
}
2024-01-11 11:42:43 +00:00
if accessToken . Cnf . Kid != v . kid {
return nil , nil , fmt . Errorf ( "expected kid %q; got %q" , v . kid , accessToken . Cnf . Kid )
2023-05-16 12:33:11 +00:00
}
2024-01-11 11:42:43 +00:00
if accessToken . ClientID != v . wireID . ClientID {
return nil , nil , fmt . Errorf ( "invalid Wire client ID %q" , accessToken . ClientID )
2023-05-16 12:33:11 +00:00
}
2024-01-11 12:47:17 +00:00
dpopJWT , err := jose . ParseSigned ( accessToken . Proof )
2024-01-10 14:12:28 +00:00
if err != nil {
return nil , nil , fmt . Errorf ( "invalid Wire DPoP token: %w" , err )
}
var dpopToken wireDpopToken
2024-01-11 12:47:17 +00:00
if err := dpopJWT . UnsafeClaimsWithoutVerification ( & dpopToken ) ; err != nil {
2024-01-11 11:42:43 +00:00
return nil , nil , fmt . Errorf ( "failed parsing Wire DPoP token: %w" , err )
2023-05-26 08:23:24 +00:00
}
2024-01-11 12:47:17 +00:00
challenge , ok := dpopToken [ "chal" ] . ( string )
if ! ok {
return nil , nil , fmt . Errorf ( "invalid challenge in Wire DPoP token" )
}
if challenge != v . challenge . Token {
return nil , nil , fmt . Errorf ( "invalid Wire DPoP challenge %q" , challenge )
}
2023-09-12 15:12:25 +00:00
2024-01-10 14:12:28 +00:00
handle , ok := dpopToken [ "handle" ] . ( string )
if ! ok {
2024-01-11 11:42:43 +00:00
return nil , nil , fmt . Errorf ( "invalid handle in Wire DPoP token" )
2023-05-16 12:33:11 +00:00
}
2024-01-11 12:47:17 +00:00
if handle != v . wireID . Handle {
return nil , nil , fmt . Errorf ( "invalid Wire client handle %q" , handle )
}
2024-01-10 14:12:28 +00:00
2024-01-11 11:42:43 +00:00
// TODO(hs): what to do with max expiry?
// maxExpiry:= strconv.FormatInt(time.Now().Add(time.Hour*24*365).Unix(), 10),
2024-01-10 14:12:28 +00:00
// "--max-expiry",
// expiry,
return & accessToken , & dpopToken , nil
2022-12-08 17:10:36 +00:00
}
2023-01-27 14:36:48 +00:00
type payloadType struct {
2022-07-15 00:10:03 +00:00
AttObj string ` json:"attObj" `
Error string ` json:"error" `
2022-04-08 20:49:11 +00:00
}
2023-01-27 14:36:48 +00:00
type attestationObject struct {
2022-04-08 20:49:11 +00:00
Format string ` json:"fmt" `
AttStatement map [ string ] interface { } ` json:"attStmt,omitempty" `
}
// TODO(bweeks): move attestation verification to a shared package.
2022-04-02 02:56:05 +00:00
func deviceAttest01Validate ( ctx context . Context , ch * Challenge , db DB , jwk * jose . JSONWebKey , payload [ ] byte ) error {
2023-02-10 00:48:43 +00:00
// Load authorization to store the key fingerprint.
az , err := db . GetAuthorization ( ctx , ch . AuthorizationID )
if err != nil {
return WrapErrorISE ( err , "error loading authorization" )
}
// Parse payload.
2023-01-27 14:36:48 +00:00
var p payloadType
2022-04-08 20:49:11 +00:00
if err := json . Unmarshal ( payload , & p ) ; err != nil {
return WrapErrorISE ( err , "error unmarshalling JSON" )
}
2022-07-15 00:10:03 +00:00
if p . Error != "" {
2022-04-08 20:49:11 +00:00
return storeError ( ctx , db , ch , true , NewError ( ErrorRejectedIdentifierType ,
2022-07-15 00:10:03 +00:00
"payload contained error: %v" , p . Error ) )
2022-06-01 01:51:17 +00:00
}
2022-07-15 00:10:03 +00:00
attObj , err := base64 . RawURLEncoding . DecodeString ( p . AttObj )
2022-06-01 01:51:17 +00:00
if err != nil {
2022-07-15 00:10:03 +00:00
return WrapErrorISE ( err , "error base64 decoding attObj" )
2022-06-01 01:51:17 +00:00
}
2023-01-27 14:36:48 +00:00
att := attestationObject { }
2022-07-15 00:10:03 +00:00
if err := cbor . Unmarshal ( attObj , & att ) ; err != nil {
return WrapErrorISE ( err , "error unmarshalling CBOR" )
2022-06-01 01:51:17 +00:00
}
2023-07-31 10:29:07 +00:00
format := att . Format
2022-09-09 00:38:05 +00:00
prov := MustProvisionerFromContext ( ctx )
2023-07-31 10:29:07 +00:00
if ! prov . IsAttestationFormatEnabled ( ctx , provisioner . ACMEAttestationFormat ( format ) ) {
if format != "apple" && format != "step" && format != "tpm" {
2023-08-04 09:24:22 +00:00
return storeError ( ctx , db , ch , true , NewDetailedError ( ErrorBadAttestationStatementType , "unsupported attestation object format %q" , format ) )
2023-07-31 10:29:07 +00:00
}
2022-09-09 00:38:05 +00:00
return storeError ( ctx , db , ch , true ,
2023-07-31 10:29:07 +00:00
NewError ( ErrorBadAttestationStatementType , "attestation format %q is not enabled" , format ) )
2022-09-09 00:38:05 +00:00
}
2023-07-31 10:29:07 +00:00
switch format {
2022-07-15 00:10:03 +00:00
case "apple" :
2022-09-16 01:19:52 +00:00
data , err := doAppleAttestationFormat ( ctx , prov , ch , & att )
2022-07-15 00:10:03 +00:00
if err != nil {
2022-08-30 03:03:34 +00:00
var acmeError * Error
if errors . As ( err , & acmeError ) {
if acmeError . Status == 500 {
return acmeError
}
return storeError ( ctx , db , ch , true , acmeError )
}
2022-09-01 23:18:13 +00:00
return WrapErrorISE ( err , "error validating attestation" )
2022-04-08 20:49:11 +00:00
}
2023-07-28 12:25:17 +00:00
2022-08-09 22:06:52 +00:00
// Validate nonce with SHA-256 of the token.
if len ( data . Nonce ) != 0 {
2022-07-15 00:10:03 +00:00
sum := sha256 . Sum256 ( [ ] byte ( ch . Token ) )
2022-08-09 22:06:52 +00:00
if subtle . ConstantTimeCompare ( data . Nonce , sum [ : ] ) != 1 {
2023-08-04 09:24:22 +00:00
return storeError ( ctx , db , ch , true , NewDetailedError ( ErrorBadAttestationStatementType , "challenge token does not match" ) )
2022-07-15 00:10:03 +00:00
}
}
2022-04-08 20:49:11 +00:00
2022-07-15 00:10:03 +00:00
// Validate Apple's ClientIdentifier (Identifier.Value) with device
// identifiers.
//
// Note: We might want to use an external service for this.
2023-07-28 14:28:31 +00:00
if data . UDID != ch . Value && data . SerialNumber != ch . Value {
subproblem := NewSubproblemWithIdentifier (
2023-07-31 10:11:50 +00:00
ErrorRejectedIdentifierType ,
2023-07-28 12:25:17 +00:00
Identifier { Type : "permanent-identifier" , Value : ch . Value } ,
2023-08-03 12:45:00 +00:00
"challenge identifier %q doesn't match any of the attested hardware identifiers %q" , ch . Value , [ ] string { data . UDID , data . SerialNumber } ,
2023-07-28 12:25:17 +00:00
)
2023-08-04 09:24:22 +00:00
return storeError ( ctx , db , ch , true , NewDetailedError ( ErrorBadAttestationStatementType , "permanent identifier does not match" ) . AddSubproblems ( subproblem ) )
2022-07-15 00:10:03 +00:00
}
2023-02-10 00:48:43 +00:00
// Update attestation key fingerprint to compare against the CSR
az . Fingerprint = data . Fingerprint
2022-08-30 02:37:30 +00:00
case "step" :
2022-09-16 01:19:52 +00:00
data , err := doStepAttestationFormat ( ctx , prov , ch , jwk , & att )
2022-08-30 02:37:30 +00:00
if err != nil {
2022-08-30 03:03:34 +00:00
var acmeError * Error
if errors . As ( err , & acmeError ) {
if acmeError . Status == 500 {
return acmeError
}
return storeError ( ctx , db , ch , true , acmeError )
}
2022-09-01 23:18:13 +00:00
return WrapErrorISE ( err , "error validating attestation" )
2022-08-30 02:37:30 +00:00
}
2022-04-08 20:49:11 +00:00
2023-01-26 12:24:25 +00:00
// Validate the YubiKey serial number from the attestation
// certificate with the challenged Order value.
2022-08-30 02:37:30 +00:00
//
// Note: We might want to use an external service for this.
if data . SerialNumber != ch . Value {
2023-01-26 12:24:25 +00:00
subproblem := NewSubproblemWithIdentifier (
2023-07-31 10:11:50 +00:00
ErrorRejectedIdentifierType ,
2023-01-26 12:24:25 +00:00
Identifier { Type : "permanent-identifier" , Value : ch . Value } ,
"challenge identifier %q doesn't match the attested hardware identifier %q" , ch . Value , data . SerialNumber ,
)
2023-08-04 09:24:22 +00:00
return storeError ( ctx , db , ch , true , NewDetailedError ( ErrorBadAttestationStatementType , "permanent identifier does not match" ) . AddSubproblems ( subproblem ) )
2022-08-30 02:37:30 +00:00
}
2023-02-10 00:48:43 +00:00
// Update attestation key fingerprint to compare against the CSR
az . Fingerprint = data . Fingerprint
2023-03-13 16:27:40 +00:00
2023-03-13 16:21:09 +00:00
case "tpm" :
2023-03-31 12:57:25 +00:00
data , err := doTPMAttestationFormat ( ctx , prov , ch , jwk , & att )
2022-09-21 12:58:03 +00:00
if err != nil {
2023-03-13 16:21:09 +00:00
var acmeError * Error
if errors . As ( err , & acmeError ) {
if acmeError . Status == 500 {
return acmeError
}
return storeError ( ctx , db , ch , true , acmeError )
}
return WrapErrorISE ( err , "error validating attestation" )
2022-09-21 12:58:03 +00:00
}
2023-03-14 12:59:16 +00:00
// TODO(hs): currently this will allow a request for which no PermanentIdentifiers have been
// extracted from the AK certificate. This is currently the case for AK certs from the CLI, as we
// haven't implemented a way for AK certs requested by the CLI to always contain the requested
// PermanentIdentifier. Omitting the check below doesn't allow just any request, as the Order can
// still fail if the challenge value isn't equal to the CSR subject.
if len ( data . PermanentIdentifiers ) > 0 && ! slices . Contains ( data . PermanentIdentifiers , ch . Value ) { // TODO(hs): add support for HardwareModuleName
subproblem := NewSubproblemWithIdentifier (
2023-07-31 10:11:50 +00:00
ErrorRejectedIdentifierType ,
2023-03-14 12:59:16 +00:00
Identifier { Type : "permanent-identifier" , Value : ch . Value } ,
2023-08-03 12:45:00 +00:00
"challenge identifier %q doesn't match any of the attested hardware identifiers %q" , ch . Value , data . PermanentIdentifiers ,
2023-03-14 12:59:16 +00:00
)
2023-08-04 09:24:22 +00:00
return storeError ( ctx , db , ch , true , NewDetailedError ( ErrorBadAttestationStatementType , "permanent identifier does not match" ) . AddSubproblems ( subproblem ) )
2023-03-14 12:59:16 +00:00
}
2022-09-21 12:58:03 +00:00
2023-03-13 22:30:39 +00:00
// Update attestation key fingerprint to compare against the CSR
az . Fingerprint = data . Fingerprint
2022-07-15 00:10:03 +00:00
default :
2023-08-04 09:24:22 +00:00
return storeError ( ctx , db , ch , true , NewDetailedError ( ErrorBadAttestationStatementType , "unsupported attestation object format %q" , format ) )
2022-04-02 02:56:05 +00:00
}
// Update and store the challenge.
ch . Status = StatusValid
ch . Error = nil
ch . ValidatedAt = clock . Now ( ) . Format ( time . RFC3339 )
2023-02-10 00:48:43 +00:00
// Store the fingerprint in the authorization.
//
// TODO: add method to update authorization and challenge atomically.
if az . Fingerprint != "" {
if err := db . UpdateAuthorization ( ctx , az ) ; err != nil {
return WrapErrorISE ( err , "error updating authorization" )
}
}
2022-04-02 02:56:05 +00:00
if err := db . UpdateChallenge ( ctx , ch ) ; err != nil {
return WrapErrorISE ( err , "error updating challenge" )
}
return nil
}
2023-03-14 08:48:44 +00:00
var (
oidSubjectAlternativeName = asn1 . ObjectIdentifier { 2 , 5 , 29 , 17 }
)
2022-09-21 12:58:03 +00:00
type tpmAttestationData struct {
2023-03-14 12:59:16 +00:00
Certificate * x509 . Certificate
VerifiedChains [ ] [ ] * x509 . Certificate
PermanentIdentifiers [ ] string
Fingerprint string
2022-09-21 12:58:03 +00:00
}
2023-04-04 10:20:31 +00:00
// coseAlgorithmIdentifier models a COSEAlgorithmIdentifier.
// Also see https://www.w3.org/TR/webauthn-2/#sctn-alg-identifier.
type coseAlgorithmIdentifier int32
const (
coseAlgES256 coseAlgorithmIdentifier = - 7
coseAlgRS256 coseAlgorithmIdentifier = - 257
2024-01-07 20:25:36 +00:00
coseAlgRS1 coseAlgorithmIdentifier = - 65535 // deprecated, but (still) often used in TPMs
2023-04-04 10:20:31 +00:00
)
2023-05-10 06:47:28 +00:00
func doTPMAttestationFormat ( _ context . Context , prov Provisioner , ch * Challenge , jwk * jose . JSONWebKey , att * attestationObject ) ( * tpmAttestationData , error ) {
2023-03-13 16:21:09 +00:00
ver , ok := att . AttStatement [ "ver" ] . ( string )
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "ver not present" )
2023-03-13 16:21:09 +00:00
}
if ver != "2.0" {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "version %q is not supported" , ver )
2023-03-13 16:21:09 +00:00
}
2022-09-21 12:58:03 +00:00
x5c , ok := att . AttStatement [ "x5c" ] . ( [ ] interface { } )
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "x5c not present" )
2022-09-21 12:58:03 +00:00
}
if len ( x5c ) == 0 {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "x5c is empty" )
2022-09-21 12:58:03 +00:00
}
2023-03-14 12:59:16 +00:00
akCertBytes , ok := x5c [ 0 ] . ( [ ] byte )
2022-09-21 12:58:03 +00:00
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "x5c is malformed" )
2022-09-21 12:58:03 +00:00
}
2023-03-14 12:59:16 +00:00
akCert , err := x509 . ParseCertificate ( akCertBytes )
2022-09-21 12:58:03 +00:00
if err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "x5c is malformed" )
2022-09-21 12:58:03 +00:00
}
intermediates := x509 . NewCertPool ( )
2023-04-03 09:54:22 +00:00
for _ , v := range x5c [ 1 : ] {
intCertBytes , vok := v . ( [ ] byte )
if ! vok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "x5c is malformed" )
2023-04-03 09:54:22 +00:00
}
intCert , err := x509 . ParseCertificate ( intCertBytes )
if err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "x5c is malformed" )
2022-09-21 12:58:03 +00:00
}
2023-04-03 09:54:22 +00:00
intermediates . AddCert ( intCert )
2022-09-21 12:58:03 +00:00
}
// TODO(hs): this can be removed when permanent-identifier/hardware-module-name are handled correctly in
// the stdlib in https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/crypto/x509/parser.go;drc=b5b2cf519fe332891c165077f3723ee74932a647;l=362,
// but I doubt that will happen.
2023-03-14 12:59:16 +00:00
if len ( akCert . UnhandledCriticalExtensions ) > 0 {
unhandledCriticalExtensions := akCert . UnhandledCriticalExtensions [ : 0 ]
for _ , extOID := range akCert . UnhandledCriticalExtensions {
2023-04-03 09:54:22 +00:00
if ! extOID . Equal ( oidSubjectAlternativeName ) {
// critical extensions other than the Subject Alternative Name remain unhandled
2022-09-21 12:58:03 +00:00
unhandledCriticalExtensions = append ( unhandledCriticalExtensions , extOID )
}
}
2023-03-14 12:59:16 +00:00
akCert . UnhandledCriticalExtensions = unhandledCriticalExtensions
2022-09-21 12:58:03 +00:00
}
roots , ok := prov . GetAttestationRoots ( )
if ! ok {
2023-04-03 09:54:22 +00:00
return nil , NewErrorISE ( "no root CA bundle available to verify the attestation certificate" )
2022-09-21 12:58:03 +00:00
}
2023-04-04 23:02:44 +00:00
// verify that the AK certificate was signed by a trusted root,
// chained to by the intermediates provided by the client. As part
// of building the verified certificate chain, the signature over the
// AK certificate is checked to be a valid signature of one of the
// provided intermediates. Signatures over the intermediates are in
// turn also verified to be valid signatures from one of the trusted
// roots.
2023-03-14 12:59:16 +00:00
verifiedChains , err := akCert . Verify ( x509 . VerifyOptions {
2022-09-21 12:58:03 +00:00
Roots : roots ,
Intermediates : intermediates ,
CurrentTime : time . Now ( ) . Truncate ( time . Second ) ,
KeyUsages : [ ] x509 . ExtKeyUsage { x509 . ExtKeyUsageAny } ,
} )
if err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "x5c is not valid" )
2022-09-21 12:58:03 +00:00
}
2023-04-04 23:02:44 +00:00
// validate additional AK certificate requirements
if err := validateAKCertificate ( akCert ) ; err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "AK certificate is not valid" )
2023-04-04 23:02:44 +00:00
}
2022-09-21 12:58:03 +00:00
// TODO(hs): implement revocation check; Verify() doesn't perform CRL check nor OCSP lookup.
2023-04-03 20:21:29 +00:00
sans , err := x509util . ParseSubjectAlternativeNames ( akCert )
2023-03-31 15:39:18 +00:00
if err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "failed parsing AK certificate Subject Alternative Names" )
2023-03-31 15:39:18 +00:00
}
2023-04-03 20:21:29 +00:00
permanentIdentifiers := make ( [ ] string , len ( sans . PermanentIdentifiers ) )
for i , pi := range sans . PermanentIdentifiers {
permanentIdentifiers [ i ] = pi . Identifier
2023-03-31 15:39:18 +00:00
}
2023-03-14 12:59:16 +00:00
// extract and validate pubArea, sig, certInfo and alg properties from the request body
2023-03-13 16:21:09 +00:00
pubArea , ok := att . AttStatement [ "pubArea" ] . ( [ ] byte )
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "invalid pubArea in attestation statement" )
2023-03-13 16:21:09 +00:00
}
2023-03-14 12:59:16 +00:00
if len ( pubArea ) == 0 {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "pubArea is empty" )
2023-03-14 12:59:16 +00:00
}
2023-03-13 16:21:09 +00:00
sig , ok := att . AttStatement [ "sig" ] . ( [ ] byte )
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "invalid sig in attestation statement" )
2023-03-13 16:21:09 +00:00
}
2023-03-14 12:59:16 +00:00
if len ( sig ) == 0 {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "sig is empty" )
2023-03-14 12:59:16 +00:00
}
certInfo , ok := att . AttStatement [ "certInfo" ] . ( [ ] byte )
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "invalid certInfo in attestation statement" )
2023-03-14 12:59:16 +00:00
}
if len ( certInfo ) == 0 {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "certInfo is empty" )
2023-03-14 12:59:16 +00:00
}
2023-03-13 16:21:09 +00:00
alg , ok := att . AttStatement [ "alg" ] . ( int64 )
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "invalid alg in attestation statement" )
2023-03-13 16:21:09 +00:00
}
2024-01-07 20:25:36 +00:00
var hash crypto . Hash
switch coseAlgorithmIdentifier ( alg ) {
case coseAlgRS256 , coseAlgES256 :
hash = crypto . SHA256
case coseAlgRS1 :
hash = crypto . SHA1
default :
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "invalid alg %d in attestation statement" , alg )
2023-03-13 16:21:09 +00:00
}
2022-09-21 12:58:03 +00:00
2023-03-14 08:48:44 +00:00
// recreate the generated key certification parameter values and verify
// the attested key using the public key of the AK.
2023-03-13 16:21:09 +00:00
certificationParameters := & attest . CertificationParameters {
2023-03-14 08:48:44 +00:00
Public : pubArea , // the public key that was attested
CreateAttestation : certInfo , // the attested properties of the key
CreateSignature : sig , // signature over the attested properties
2023-03-13 16:21:09 +00:00
}
2023-03-13 22:30:39 +00:00
verifyOpts := attest . VerifyOpts {
2023-03-14 12:59:16 +00:00
Public : akCert . PublicKey , // public key of the AK that attested the key
2023-03-13 16:21:09 +00:00
Hash : hash ,
}
2023-03-13 22:30:39 +00:00
if err = certificationParameters . Verify ( verifyOpts ) ; err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "invalid certification parameters" )
2022-09-21 12:58:03 +00:00
}
2023-03-31 12:57:25 +00:00
// decode the "certInfo" data. This won't fail, as it's also done as part of Verify().
2023-03-13 22:30:39 +00:00
tpmCertInfo , err := tpm2 . DecodeAttestationData ( certInfo )
2022-09-21 12:58:03 +00:00
if err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "failed decoding attestation data" )
2022-09-21 12:58:03 +00:00
}
2023-03-14 12:59:16 +00:00
keyAuth , err := KeyAuthorization ( ch . Token , jwk )
2023-03-13 22:30:39 +00:00
if err != nil {
2023-07-28 14:28:31 +00:00
return nil , WrapErrorISE ( err , "failed creating key auth digest" )
2023-03-13 22:30:39 +00:00
}
2023-03-14 12:59:16 +00:00
hashedKeyAuth := sha256 . Sum256 ( [ ] byte ( keyAuth ) )
2023-03-13 22:30:39 +00:00
// verify the WebAuthn object contains the expect key authorization digest, which is carried
// within the encoded `certInfo` property of the attestation statement.
2023-03-14 12:59:16 +00:00
if subtle . ConstantTimeCompare ( hashedKeyAuth [ : ] , [ ] byte ( tpmCertInfo . ExtraData ) ) == 0 {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "key authorization invalid" )
2023-03-13 22:30:39 +00:00
}
2023-03-31 12:57:25 +00:00
// decode the (attested) public key and determine its fingerprint. This won't fail, as it's also done as part of Verify().
2023-03-13 22:30:39 +00:00
pub , err := tpm2 . DecodePublic ( pubArea )
if err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "failed decoding pubArea" )
2023-03-13 22:30:39 +00:00
}
publicKey , err := pub . Key ( )
if err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "failed getting public key" )
2023-03-13 22:30:39 +00:00
}
data := & tpmAttestationData {
2023-03-31 15:39:18 +00:00
Certificate : akCert ,
VerifiedChains : verifiedChains ,
PermanentIdentifiers : permanentIdentifiers ,
2023-03-13 22:30:39 +00:00
}
if data . Fingerprint , err = keyutil . Fingerprint ( publicKey ) ; err != nil {
return nil , WrapErrorISE ( err , "error calculating key fingerprint" )
}
// TODO(hs): pass more attestation data, so that that can be used/recorded too?
return data , nil
2022-09-21 12:58:03 +00:00
}
2023-04-04 23:02:44 +00:00
var (
oidExtensionExtendedKeyUsage = asn1 . ObjectIdentifier { 2 , 5 , 29 , 37 }
oidTCGKpAIKCertificate = asn1 . ObjectIdentifier { 2 , 23 , 133 , 8 , 3 }
)
// validateAKCertifiate validates the X.509 AK certificate to be
// in accordance with the required properties. The requirements come from:
// https://www.w3.org/TR/webauthn-2/#sctn-tpm-cert-requirements.
//
2023-04-06 12:35:48 +00:00
// - Version MUST be set to 3.
// - Subject field MUST be set to empty.
// - The Subject Alternative Name extension MUST be set as defined
// in [TPMv2-EK-Profile] section 3.2.9.
// - The Extended Key Usage extension MUST contain the OID 2.23.133.8.3
// ("joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)").
// - The Basic Constraints extension MUST have the CA component set to false.
// - An Authority Information Access (AIA) extension with entry id-ad-ocsp
// and a CRL Distribution Point extension [RFC5280] are both OPTIONAL as
// the status of many attestation certificates is available through metadata
// services. See, for example, the FIDO Metadata Service.
2023-04-04 23:02:44 +00:00
func validateAKCertificate ( c * x509 . Certificate ) error {
if c . Version != 3 {
return fmt . Errorf ( "AK certificate has invalid version %d; only version 3 is allowed" , c . Version )
}
if c . Subject . String ( ) != "" {
2023-04-06 12:35:48 +00:00
return fmt . Errorf ( "AK certificate subject must be empty; got %q" , c . Subject )
2023-04-04 23:02:44 +00:00
}
2023-04-06 12:35:48 +00:00
if c . IsCA {
return errors . New ( "AK certificate must not be a CA" )
2023-04-04 23:02:44 +00:00
}
if err := validateAKCertificateExtendedKeyUsage ( c ) ; err != nil {
return err
}
2023-05-10 06:47:28 +00:00
return validateAKCertificateSubjectAlternativeNames ( c )
2023-04-04 23:02:44 +00:00
}
2023-04-06 12:35:48 +00:00
// validateAKCertificateSubjectAlternativeNames checks if the AK certificate
// has TPM hardware details set.
2023-04-04 23:02:44 +00:00
func validateAKCertificateSubjectAlternativeNames ( c * x509 . Certificate ) error {
sans , err := x509util . ParseSubjectAlternativeNames ( c )
if err != nil {
return fmt . Errorf ( "failed parsing AK certificate Subject Alternative Names: %w" , err )
}
details := sans . TPMHardwareDetails
manufacturer , model , version := details . Manufacturer , details . Model , details . Version
switch {
case manufacturer == "" :
return errors . New ( "missing TPM manufacturer" )
case model == "" :
return errors . New ( "missing TPM model" )
case version == "" :
return errors . New ( "missing TPM version" )
}
return nil
}
// validateAKCertificateExtendedKeyUsage checks if the AK certificate
// has the "tcg-kp-AIKCertificate" Extended Key Usage set.
func validateAKCertificateExtendedKeyUsage ( c * x509 . Certificate ) error {
var (
valid = false
ekus [ ] asn1 . ObjectIdentifier
)
for _ , ext := range c . Extensions {
if ext . Id . Equal ( oidExtensionExtendedKeyUsage ) {
if _ , err := asn1 . Unmarshal ( ext . Value , & ekus ) ; err != nil || ! ekus [ 0 ] . Equal ( oidTCGKpAIKCertificate ) {
return errors . New ( "AK certificate is missing Extended Key Usage value tcg-kp-AIKCertificate (2.23.133.8.3)" )
}
valid = true
}
}
if ! valid {
return errors . New ( "AK certificate is missing Extended Key Usage extension" )
}
return nil
}
2022-07-15 00:10:03 +00:00
// Apple Enterprise Attestation Root CA from
// https://www.apple.com/certificateauthority/private/
const appleEnterpriseAttestationRootCA = ` -- -- - BEGIN CERTIFICATE -- -- -
MIICJDCCAamgAwIBAgIUQsDCuyxyfFxeq / bxpm8frF15hzcwCgYIKoZIzj0EAwMw
UTEtMCsGA1UEAwwkQXBwbGUgRW50ZXJwcmlzZSBBdHRlc3RhdGlvbiBSb290IENB
MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzAeFw0yMjAyMTYxOTAx
MjRaFw00NzAyMjAwMDAwMDBaMFExLTArBgNVBAMMJEFwcGxlIEVudGVycHJpc2Ug
QXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UE
BhMCVVMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT6Jigq + Ps9Q4CoT8t8q + UnOe2p
oT9nRaUfGhBTbgvqSGXPjVkbYlIWYO + 1 zPk2Sz9hQ5ozzmLrPmTBgEWRcHjA2 / y7
7 GEicps9wn2tj + G89l3INNDKETdxSPPIZpPj8VmjQjBAMA8GA1UdEwEB / wQFMAMB
Af8wHQYDVR0OBBYEFPNqTQGd8muBpV5du + UIbVbi + d66MA4GA1UdDwEB / wQEAwIB
BjAKBggqhkjOPQQDAwNpADBmAjEA1xpWmTLSpr1VH4f8Ypk8f3jMUKYz4QPG8mL5
8 m9sX / b2 + eXpTv2pH4RZgJjucnbcAjEA4ZSB6S45FlPuS / u4pTnzoz632rA + xW / T
ZwFEh9bhKjJ + 5 VQ9 / Do1os0u3LEkgN / r
-- -- - END CERTIFICATE -- -- - `
var (
oidAppleSerialNumber = asn1 . ObjectIdentifier { 1 , 2 , 840 , 113635 , 100 , 8 , 9 , 1 }
oidAppleUniqueDeviceIdentifier = asn1 . ObjectIdentifier { 1 , 2 , 840 , 113635 , 100 , 8 , 9 , 2 }
oidAppleSecureEnclaveProcessorOSVersion = asn1 . ObjectIdentifier { 1 , 2 , 840 , 113635 , 100 , 8 , 10 , 2 }
oidAppleNonce = asn1 . ObjectIdentifier { 1 , 2 , 840 , 113635 , 100 , 8 , 11 , 1 }
)
2022-06-07 04:09:31 +00:00
2022-07-15 00:10:03 +00:00
type appleAttestationData struct {
2022-08-09 22:06:52 +00:00
Nonce [ ] byte
2022-07-15 00:10:03 +00:00
SerialNumber string
UDID string
SEPVersion string
Certificate * x509 . Certificate
2023-02-10 00:48:43 +00:00
Fingerprint string
2022-07-15 00:10:03 +00:00
}
2022-06-07 04:09:31 +00:00
2023-05-10 06:47:28 +00:00
func doAppleAttestationFormat ( _ context . Context , prov Provisioner , _ * Challenge , att * attestationObject ) ( * appleAttestationData , error ) {
2022-09-16 01:19:52 +00:00
// Use configured or default attestation roots if none is configured.
roots , ok := prov . GetAttestationRoots ( )
if ! ok {
root , err := pemutil . ParseCertificate ( [ ] byte ( appleEnterpriseAttestationRootCA ) )
if err != nil {
return nil , WrapErrorISE ( err , "error parsing apple enterprise ca" )
}
roots = x509 . NewCertPool ( )
roots . AddCert ( root )
2022-06-07 04:09:31 +00:00
}
2022-07-15 00:10:03 +00:00
x5c , ok := att . AttStatement [ "x5c" ] . ( [ ] interface { } )
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "x5c not present" )
2022-06-07 04:09:31 +00:00
}
2022-06-22 19:43:24 +00:00
if len ( x5c ) == 0 {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "x5c is empty" )
2022-06-22 19:43:24 +00:00
}
2022-07-15 00:10:03 +00:00
der , ok := x5c [ 0 ] . ( [ ] byte )
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "x5c is malformed" )
2022-06-07 04:09:31 +00:00
}
2022-07-15 00:10:03 +00:00
leaf , err := x509 . ParseCertificate ( der )
2022-06-07 04:09:31 +00:00
if err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "x5c is malformed" )
2022-06-07 04:09:31 +00:00
}
2022-07-15 00:10:03 +00:00
intermediates := x509 . NewCertPool ( )
for _ , v := range x5c [ 1 : ] {
der , ok = v . ( [ ] byte )
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "x5c is malformed" )
2022-07-15 00:10:03 +00:00
}
cert , err := x509 . ParseCertificate ( der )
if err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "x5c is malformed" )
2022-07-15 00:10:03 +00:00
}
intermediates . AddCert ( cert )
}
if _ , err := leaf . Verify ( x509 . VerifyOptions {
Intermediates : intermediates ,
Roots : roots ,
CurrentTime : time . Now ( ) . Truncate ( time . Second ) ,
KeyUsages : [ ] x509 . ExtKeyUsage { x509 . ExtKeyUsageAny } ,
} ) ; err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "x5c is not valid" )
2022-07-15 00:10:03 +00:00
}
data := & appleAttestationData {
Certificate : leaf ,
}
2023-02-10 00:48:43 +00:00
if data . Fingerprint , err = keyutil . Fingerprint ( leaf . PublicKey ) ; err != nil {
return nil , WrapErrorISE ( err , "error calculating key fingerprint" )
}
2022-07-15 00:10:03 +00:00
for _ , ext := range leaf . Extensions {
switch {
case ext . Id . Equal ( oidAppleSerialNumber ) :
data . SerialNumber = string ( ext . Value )
case ext . Id . Equal ( oidAppleUniqueDeviceIdentifier ) :
data . UDID = string ( ext . Value )
case ext . Id . Equal ( oidAppleSecureEnclaveProcessorOSVersion ) :
data . SEPVersion = string ( ext . Value )
case ext . Id . Equal ( oidAppleNonce ) :
2022-08-09 22:06:52 +00:00
data . Nonce = ext . Value
2022-07-15 00:10:03 +00:00
}
2022-06-07 04:09:31 +00:00
}
2022-07-15 00:10:03 +00:00
return data , nil
2022-06-07 04:09:31 +00:00
}
2022-08-30 02:37:30 +00:00
// Yubico PIV Root CA Serial 263751
// https://developers.yubico.com/PIV/Introduction/piv-attestation-ca.pem
const yubicoPIVRootCA = ` -- -- - BEGIN CERTIFICATE -- -- -
MIIDFzCCAf + gAwIBAgIDBAZHMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNVBAMMIFl1
YmljbyBQSVYgUm9vdCBDQSBTZXJpYWwgMjYzNzUxMCAXDTE2MDMxNDAwMDAwMFoY
DzIwNTIwNDE3MDAwMDAwWjArMSkwJwYDVQQDDCBZdWJpY28gUElWIFJvb3QgQ0Eg
U2VyaWFsIDI2Mzc1MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMN2
cMTNR6YCdcTFRxuPy31PabRn5m6pJ + nSE0HRWpoaM8fc8wHC + Tmb98jmNvhWNE2E
ilU85uYKfEFP9d6Q2GmytqBnxZsAa3KqZiCCx2LwQ4iYEOb1llgotVr / whEpdVOq
joU0P5e1j1y7OfwOvky / + AXIN / 9 Xp0VFlYRk2tQ9GcdYKDmqU + db9iKwpAzid4oH
BVLIhmD3pvkWaRA2H3DA9t7H / HNq5v3OiO1jyLZeKqZoMbPObrxqDg + 9 fOdShzgf
wCqgT3XVmTeiwvBSTctyi9mHQfYd2DwkaqxRnLbNVyK9zl + DzjSGp9IhVPiVtGet
X02dxhQnGS7K6BO0Qe8CAwEAAaNCMEAwHQYDVR0OBBYEFMpfyvLEojGc6SJf8ez0
1 d8Cv4O / MA8GA1UdEwQIMAYBAf8CAQEwDgYDVR0PAQH / BAQDAgEGMA0GCSqGSIb3
DQEBCwUAA4IBAQBc7Ih8Bc1fkC + FyN1fhjWioBCMr3vjneh7MLbA6kSoyWF70N3s
XhbXvT4eRh0hvxqvMZNjPU / VlRn6gLVtoEikDLrYFXN6Hh6Wmyy1GTnspnOvMvz2
lLKuym9KYdYLDgnj3BeAvzIhVzzYSeU77 / Cupofj093OuAswW0jYvXsGTyix6B3d
bW5yWvyS9zNXaqGaUmP3U9 / b6DlHdDogMLu3VLpBB9bm5bjaKWWJYgWltCVgUbFq
Fqyi4 + JE014cSgR57Jcu3dZiehB6UtAPgad9L5cNvua / IWRmm + ANy3O2LH ++ Pyl8
SREzU8onbBsjMg9QDiSf5oJLKvd / Ren + zGY7
-- -- - END CERTIFICATE -- -- - `
// Serial number of the YubiKey, encoded as an integer.
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
var oidYubicoSerialNumber = asn1 . ObjectIdentifier { 1 , 3 , 6 , 1 , 4 , 1 , 41482 , 3 , 7 }
type stepAttestationData struct {
Certificate * x509 . Certificate
SerialNumber string
2023-02-10 00:48:43 +00:00
Fingerprint string
2022-08-30 02:37:30 +00:00
}
2023-05-10 06:47:28 +00:00
func doStepAttestationFormat ( _ context . Context , prov Provisioner , ch * Challenge , jwk * jose . JSONWebKey , att * attestationObject ) ( * stepAttestationData , error ) {
2022-09-16 01:19:52 +00:00
// Use configured or default attestation roots if none is configured.
roots , ok := prov . GetAttestationRoots ( )
if ! ok {
root , err := pemutil . ParseCertificate ( [ ] byte ( yubicoPIVRootCA ) )
if err != nil {
return nil , WrapErrorISE ( err , "error parsing root ca" )
}
roots = x509 . NewCertPool ( )
roots . AddCert ( root )
2022-08-30 02:37:30 +00:00
}
2022-09-01 17:45:31 +00:00
// Extract x5c and verify certificate
2022-08-30 02:37:30 +00:00
x5c , ok := att . AttStatement [ "x5c" ] . ( [ ] interface { } )
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "x5c not present" )
2022-08-30 02:37:30 +00:00
}
if len ( x5c ) == 0 {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorRejectedIdentifierType , "x5c is empty" )
2022-08-30 02:37:30 +00:00
}
der , ok := x5c [ 0 ] . ( [ ] byte )
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "x5c is malformed" )
2022-08-30 02:37:30 +00:00
}
leaf , err := x509 . ParseCertificate ( der )
if err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "x5c is malformed" )
2022-08-30 02:37:30 +00:00
}
intermediates := x509 . NewCertPool ( )
for _ , v := range x5c [ 1 : ] {
der , ok = v . ( [ ] byte )
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "x5c is malformed" )
2022-08-30 02:37:30 +00:00
}
cert , err := x509 . ParseCertificate ( der )
if err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "x5c is malformed" )
2022-08-30 02:37:30 +00:00
}
intermediates . AddCert ( cert )
}
if _ , err := leaf . Verify ( x509 . VerifyOptions {
Intermediates : intermediates ,
Roots : roots ,
CurrentTime : time . Now ( ) . Truncate ( time . Second ) ,
KeyUsages : [ ] x509 . ExtKeyUsage { x509 . ExtKeyUsageAny } ,
} ) ; err != nil {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "x5c is not valid" )
2022-08-30 02:37:30 +00:00
}
2022-09-01 17:45:31 +00:00
// Verify proof of possession of private key validating the key
// authorization. Per recommendation at
// https://w3c.github.io/webauthn/#sctn-signature-attestation-types the
// signature is CBOR-encoded.
var sig [ ] byte
csig , ok := att . AttStatement [ "sig" ] . ( [ ] byte )
if ! ok {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "sig not present" )
2022-09-01 17:45:31 +00:00
}
if err := cbor . Unmarshal ( csig , & sig ) ; err != nil {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "sig is malformed" )
2022-09-01 17:45:31 +00:00
}
keyAuth , err := KeyAuthorization ( ch . Token , jwk )
if err != nil {
return nil , err
}
switch pub := leaf . PublicKey . ( type ) {
case * ecdsa . PublicKey :
if pub . Curve != elliptic . P256 ( ) {
2023-08-04 09:24:22 +00:00
return nil , WrapDetailedError ( ErrorBadAttestationStatementType , err , "unsupported elliptic curve %s" , pub . Curve )
2022-09-01 17:45:31 +00:00
}
sum := sha256 . Sum256 ( [ ] byte ( keyAuth ) )
if ! ecdsa . VerifyASN1 ( pub , sum [ : ] , sig ) {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "failed to validate signature" )
2022-09-01 17:45:31 +00:00
}
case * rsa . PublicKey :
sum := sha256 . Sum256 ( [ ] byte ( keyAuth ) )
if err := rsa . VerifyPKCS1v15 ( pub , crypto . SHA256 , sum [ : ] , sig ) ; err != nil {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "failed to validate signature" )
2022-09-01 17:45:31 +00:00
}
case ed25519 . PublicKey :
if ! ed25519 . Verify ( pub , [ ] byte ( keyAuth ) , sig ) {
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "failed to validate signature" )
2022-09-01 17:45:31 +00:00
}
default :
2023-08-04 09:24:22 +00:00
return nil , NewDetailedError ( ErrorBadAttestationStatementType , "unsupported public key type %T" , pub )
2022-09-01 17:45:31 +00:00
}
2022-09-01 23:18:13 +00:00
// Parse attestation data:
// TODO(mariano): add support for other extensions.
2022-08-30 02:37:30 +00:00
data := & stepAttestationData {
Certificate : leaf ,
}
2023-02-10 00:48:43 +00:00
if data . Fingerprint , err = keyutil . Fingerprint ( leaf . PublicKey ) ; err != nil {
return nil , WrapErrorISE ( err , "error calculating key fingerprint" )
}
2022-08-30 02:37:30 +00:00
for _ , ext := range leaf . Extensions {
2022-09-01 23:18:13 +00:00
if ! ext . Id . Equal ( oidYubicoSerialNumber ) {
continue
}
var serialNumber int
rest , err := asn1 . Unmarshal ( ext . Value , & serialNumber )
if err != nil || len ( rest ) > 0 {
2022-09-08 17:46:58 +00:00
return nil , WrapError ( ErrorBadAttestationStatementType , err , "error parsing serial number" )
2022-08-30 02:37:30 +00:00
}
2022-09-01 23:18:13 +00:00
data . SerialNumber = strconv . Itoa ( serialNumber )
break
2022-08-30 02:37:30 +00:00
}
return data , nil
}
2021-06-18 15:27:35 +00:00
// serverName determines the SNI HostName to set based on an acme.Challenge
2021-06-25 12:07:40 +00:00
// for TLS-ALPN-01 challenges RFC8738 states that, if HostName is an IP, it
2021-06-18 15:27:35 +00:00
// should be the ARPA address https://datatracker.ietf.org/doc/html/rfc8738#section-6.
// It also references TLS Extensions [RFC6066].
func serverName ( ch * Challenge ) string {
var serverName string
ip := net . ParseIP ( ch . Value )
if ip != nil {
serverName = reverseAddr ( ip )
} else {
serverName = ch . Value
}
return serverName
}
2021-06-03 22:01:43 +00:00
// reverseaddr returns the in-addr.arpa. or ip6.arpa. hostname of the IP
// address addr suitable for rDNS (PTR) record lookup or an error if it fails
// to parse the IP address.
// Implementation taken and adapted from https://golang.org/src/net/dnsclient.go?s=780:834#L20
func reverseAddr ( ip net . IP ) ( arpa string ) {
if ip . To4 ( ) != nil {
return uitoa ( uint ( ip [ 15 ] ) ) + "." + uitoa ( uint ( ip [ 14 ] ) ) + "." + uitoa ( uint ( ip [ 13 ] ) ) + "." + uitoa ( uint ( ip [ 12 ] ) ) + ".in-addr.arpa."
}
// Must be IPv6
buf := make ( [ ] byte , 0 , len ( ip ) * 4 + len ( "ip6.arpa." ) )
// Add it, in reverse, to the buffer
for i := len ( ip ) - 1 ; i >= 0 ; i -- {
v := ip [ i ]
buf = append ( buf , hexit [ v & 0xF ] ,
'.' ,
hexit [ v >> 4 ] ,
'.' )
}
// Append "ip6.arpa." and return (buf already has the final .)
buf = append ( buf , "ip6.arpa." ... )
return string ( buf )
}
// Convert unsigned integer to decimal string.
// Implementation taken from https://golang.org/src/net/parse.go
func uitoa ( val uint ) string {
if val == 0 { // avoid string allocation
return "0"
}
var buf [ 20 ] byte // big enough for 64bit value base 10
i := len ( buf ) - 1
for val >= 10 {
2022-09-21 12:58:03 +00:00
v := val / 10
buf [ i ] = byte ( '0' + val - v * 10 )
2021-06-03 22:01:43 +00:00
i --
2022-09-21 12:58:03 +00:00
val = v
2021-06-03 22:01:43 +00:00
}
// val < 10
buf [ i ] = byte ( '0' + val )
return string ( buf [ i : ] )
}
const hexit = "0123456789abcdef"
2021-02-28 01:05:37 +00:00
// KeyAuthorization creates the ACME key authorization value from a token
// and a jwk.
func KeyAuthorization ( token string , jwk * jose . JSONWebKey ) ( string , error ) {
thumbprint , err := jwk . Thumbprint ( crypto . SHA256 )
if err != nil {
2021-03-05 07:10:46 +00:00
return "" , WrapErrorISE ( err , "error generating JWK thumbprint" )
2021-02-28 01:05:37 +00:00
}
encPrint := base64 . RawURLEncoding . EncodeToString ( thumbprint )
return fmt . Sprintf ( "%s.%s" , token , encPrint ) , nil
}
// storeError the given error to an ACME error and saves using the DB interface.
2021-03-30 05:58:26 +00:00
func storeError ( ctx context . Context , db DB , ch * Challenge , markInvalid bool , err * Error ) error {
2021-03-01 07:33:18 +00:00
ch . Error = err
2021-03-30 05:58:26 +00:00
if markInvalid {
ch . Status = StatusInvalid
}
2021-02-28 01:05:37 +00:00
if err := db . UpdateChallenge ( ctx , ch ) ; err != nil {
2021-03-05 07:10:46 +00:00
return WrapErrorISE ( err , "failure saving error to acme challenge" )
2021-02-28 01:05:37 +00:00
}
return nil
}